Skip to content

Commit

Permalink
Merge pull request #42 from RobertD502/rework_config_flow
Browse files Browse the repository at this point in the history
Rework config flow, add Reauth Flow, add Options Flow (polling interval), add zip release github workflow, set min version to 2024.12
  • Loading branch information
gdgib authored Jan 14, 2025
2 parents f4e6553 + 3f95f17 commit cba97bd
Show file tree
Hide file tree
Showing 41 changed files with 1,001 additions and 141 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/setup-python@v4.7.0
- uses: pre-commit/action@v3.0.0
- uses: actions/checkout@v4.2.2
- uses: actions/setup-python@v5.3.0
with:
python-version-file: '.python-version'
- uses: pre-commit/action@v3.0.1
28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: "Release"

on:
release:
types: [published, edited]

permissions: {}

jobs:
release:
name: "Release"
runs-on: "ubuntu-latest"
permissions:
contents: write
steps:
- name: "Checkout the repository"
uses: "actions/checkout@v4.2.2"

- name: "ZIP the integration directory"
shell: "bash"
run: |
cd "${{ github.workspace }}/custom_components/vesync"
zip vesync.zip -r ./
- name: "Upload the ZIP file to the release"
uses: softprops/action-gh-release@v0.1.15
with:
files: ${{ github.workspace }}/custom_components/vesync/vesync.zip
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ This integration will override the core VeSync integration.

### HACS

[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=haext&repository=custom_vesync&category=integration)

This integration can be installed by adding this repository to HACS __AS A CUSTOM REPOSITORY__, then searching for `Custom VeSync`, and choosing install. Reboot Home Assistant and configure the 'VeSync' integration via the integrations page or press the blue button below.

[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=vesync)
Expand All @@ -40,7 +42,8 @@ You can make sure the custom integration is in use by looking for the following

Navigate to the Vesync integration and click on `Enable debug logging`. Restart Home Assistant. Give it a few minutes and navigate back to the Vesync integration and disable debug logging. A local log file will get downloaded to your device.

![image](https://github.com/RobertD502/custom_vesync/assets/52541649/c556458c-a0a6-4432-acec-1200fc561d79)
![image](https://github.com/user-attachments/assets/9eec21fb-5414-4fb7-8fbb-c35d24e62555)


#### YAML Method

Expand Down
58 changes: 50 additions & 8 deletions custom_components/vesync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
Expand All @@ -14,7 +15,9 @@
from .common import async_process_devices
from .const import (
DOMAIN,
POLLING_INTERVAL,
SERVICE_UPDATE_DEVS,
UPDATE_LISTENER,
VS_BINARY_SENSORS,
VS_BUTTON,
VS_DISCOVERY,
Expand Down Expand Up @@ -56,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b

if not login:
_LOGGER.error("Unable to login to the VeSync server")
return False
raise ConfigEntryAuthFailed("Error logging in with username and password")

hass.data[DOMAIN] = {config_entry.entry_id: {}}
hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER] = manager
Expand All @@ -74,11 +77,11 @@ async def async_update_data():
_LOGGER,
name="vesync",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
update_interval=timedelta(seconds=config_entry.options[POLLING_INTERVAL]),
)

# Fetch initial data so we have data when entities subscribe
await coordinator.async_refresh()
await coordinator.async_config_entry_first_refresh()

# Store the coordinator instance in hass.data
hass.data[DOMAIN][config_entry.entry_id]["coordinator"] = coordinator
Expand All @@ -92,8 +95,15 @@ async def async_update_data():
hass.data[DOMAIN][config_entry.entry_id][vs_p].extend(device_dict[vs_p])
platforms_list.append(p)

# Store loaded platforms
hass.data[DOMAIN][config_entry.entry_id]["loaded_platforms"] = platforms_list

await hass.config_entries.async_forward_entry_setups(config_entry, platforms_list)

# Add update listener and store it
update_listener = config_entry.add_update_listener(async_update_options)
hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener

async def async_new_device_discovery(service: ServiceCall) -> None:
"""Discover if new devices should be added."""
manager = hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER]
Expand Down Expand Up @@ -131,10 +141,42 @@ async def _add_new_devices(platform: str) -> None:

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, list(PLATFORMS.keys())
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

loaded_platforms = hass.data[DOMAIN][entry.entry_id]["loaded_platforms"]
if unload_ok := await hass.config_entries.async_unload_platforms(
entry, loaded_platforms
):
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok


async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""

if entry.version == 1:
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]

_LOGGER.debug("Migrating VeSync config entry")

hass.config_entries.async_update_entry(
entry,
version=2,
data={
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
options={
POLLING_INTERVAL: 60,
},
)

return True


async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""

await hass.config_entries.async_reload(entry.entry_id)
181 changes: 141 additions & 40 deletions custom_components/vesync/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Config flow utilities."""

from __future__ import annotations

import logging
from collections import OrderedDict
from collections.abc import Mapping
from typing import Any

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import dhcp
Expand All @@ -11,59 +15,130 @@
from homeassistant.data_entry_flow import FlowResult
from pyvesync.vesync import VeSync

from .const import DOMAIN
from .const import DOMAIN, POLLING_INTERVAL

_LOGGER = logging.getLogger(__name__)


DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(POLLING_INTERVAL, default=60): int,
}
)


def reauth_schema(
def_username: str | vol.UNDEFINED = vol.UNDEFINED,
def_password: str | vol.UNDEFINED = vol.UNDEFINED,
def_poll: int | vol.UNDEFINED = 60,
) -> dict[vol.Marker, Any]:
"""Return schema for reauth flow with optional default value."""

return {
vol.Required(CONF_USERNAME, default=def_username): cv.string,
vol.Required(CONF_PASSWORD, default=def_password): cv.string,
vol.Required(POLLING_INTERVAL, default=def_poll): int,
}


class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

VERSION = 1
VERSION = 2

def __init__(self) -> None:
"""Instantiate config flow."""
self._username = None
self._password = None
self.data_schema = OrderedDict()
self.data_schema[vol.Required(CONF_USERNAME)] = str
self.data_schema[vol.Required(CONF_PASSWORD)] = str
entry: config_entries.ConfigEntry | None

@staticmethod
@callback
def _show_form(self, errors=None):
"""Show form to the user."""
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> VeSyncOptionsFlowHandler:
"""Get the options flow for this handler."""

return VeSyncOptionsFlowHandler()

async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle re-authentication with VeSync."""

self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm re-authentication with VeSync."""

errors: dict[str, str] = {}
if user_input:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
polling_interval = user_input[POLLING_INTERVAL]
manager = VeSync(username, password)
login = await self.hass.async_add_executor_job(manager.login)
if not login:
errors["base"] = "invalid_auth"
else:
assert self.entry is not None

self.hass.config_entries.async_update_entry(
self.entry,
data={
**self.entry.data,
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
options={
POLLING_INTERVAL: polling_interval,
},
)

await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(self.data_schema),
errors=errors or {},
step_id="reauth_confirm",
data_schema=vol.Schema(
reauth_schema(
self.entry.data[CONF_USERNAME],
self.entry.data[CONF_PASSWORD],
self.entry.options[POLLING_INTERVAL],
)
),
errors=errors,
)

async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow start."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")

if not user_input:
return self._show_form()

self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]

manager = VeSync(self._username, self._password)
login = await self.hass.async_add_executor_job(manager.login)
await self.async_set_unique_id(f"{self._username}-{manager.account_id}")
self._abort_if_unique_id_configured()

return (
self.async_create_entry(
title=self._username,
data={
CONF_USERNAME: self._username,
CONF_PASSWORD: self._password,
},
)
if login
else self._show_form(errors={"base": "invalid_auth"})

errors: dict[str, str] = {}

if user_input:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
polling_interval = user_input[POLLING_INTERVAL]
manager = VeSync(username, password)
login = await self.hass.async_add_executor_job(manager.login)
if not login:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(f"{username}-{manager.account_id}")
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=username,
data={CONF_USERNAME: username, CONF_PASSWORD: password},
options={
POLLING_INTERVAL: polling_interval,
},
)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
)

async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
Expand All @@ -73,3 +148,29 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
_LOGGER.debug("DHCP discovery detected device %s", hostname)
self.context["title_placeholders"] = {"gateway_id": hostname}
return await self.async_step_user()


class VeSyncOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle VeSync integration options."""

async def async_step_init(self, user_input=None):
"""Manage options."""

return await self.async_step_vesync_options()

async def async_step_vesync_options(self, user_input=None):
"""Manage the VeSync options."""

if user_input is not None:
return self.async_create_entry(title="", data=user_input)

options = {
vol.Required(
POLLING_INTERVAL,
default=self.config_entry.options.get(POLLING_INTERVAL, 60),
): int,
}

return self.async_show_form(
step_id="vesync_options", data_schema=vol.Schema(options)
)
2 changes: 2 additions & 0 deletions custom_components/vesync/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from homeassistant.const import UnitOfTemperature, UnitOfTime

DOMAIN = "vesync"
POLLING_INTERVAL = "polling_interval"
UPDATE_LISTENER = "update_listener"
VS_DISCOVERY = "vesync_discovery_{}"
SERVICE_UPDATE_DEVS = "update_devices"

Expand Down
Loading

0 comments on commit cba97bd

Please sign in to comment.