Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6ed6d0b
switcher start s11 integration
YogevBokobza Aug 11, 2024
c2a3937
switcher linting
YogevBokobza Aug 11, 2024
8b55ab4
switcher starting reauth logic
YogevBokobza Aug 11, 2024
91d270f
switcher fix linting
YogevBokobza Aug 11, 2024
a4bbfef
switcher fix linting
YogevBokobza Aug 12, 2024
115e69e
switcher remove get_circuit_number
YogevBokobza Aug 12, 2024
701b3a5
switcher adding support for validate token
YogevBokobza Aug 12, 2024
f1dbb11
switcher fix initial auth for new devices and fix strings
YogevBokobza Aug 13, 2024
d85b6cf
switcher fix linting
YogevBokobza Aug 13, 2024
0f69c6c
switcher fix utils
YogevBokobza Aug 13, 2024
17129ab
Revert "switcher fix utils"
YogevBokobza Aug 13, 2024
53af238
switcher revert and test
YogevBokobza Aug 13, 2024
06cc455
switcher fix validate logic and strings
YogevBokobza Aug 13, 2024
a295a12
switcher add tests to improve coverage
YogevBokobza Aug 14, 2024
8903c53
switcher adding tests
YogevBokobza Aug 14, 2024
523eac7
switcher adding test
YogevBokobza Aug 14, 2024
5b3ab35
switcher revert back things
YogevBokobza Aug 15, 2024
c110c7b
switcher fix based on requested changes
YogevBokobza Aug 26, 2024
9b016f7
switcher tests fixes
YogevBokobza Aug 26, 2024
b38e04e
switcher fix based on requested changes
YogevBokobza Aug 26, 2024
3bec02f
switcher remove single_instance_allowed code and added tests
YogevBokobza Sep 5, 2024
b938eba
Update config_flow.py
YogevBokobza Sep 5, 2024
79ab2cc
switcher fix comment
YogevBokobza Sep 5, 2024
de515ed
switcher fix tests
YogevBokobza Sep 12, 2024
ca180d9
switcher lint
YogevBokobza Sep 12, 2024
85ba91a
switcehr fix based on requested changes
YogevBokobza Sep 13, 2024
0328d28
switche fix lint
YogevBokobza Sep 13, 2024
c8aa0d2
switcher small rename fix
YogevBokobza Sep 14, 2024
0419027
switcher fix based on requested changes
YogevBokobza Sep 20, 2024
83f48b7
switcher fix based on requested changes
YogevBokobza Sep 20, 2024
d8820d3
switcher fix based on requested changes
YogevBokobza Sep 20, 2024
74c9d2e
Update tests/components/switcher_kis/test_config_flow.py
YogevBokobza Sep 20, 2024
5aa4898
Update tests/components/switcher_kis/test_config_flow.py
YogevBokobza Sep 20, 2024
27d52da
Update tests/components/switcher_kis/test_config_flow.py
YogevBokobza Sep 20, 2024
201425c
Update tests/components/switcher_kis/test_config_flow.py
thecode Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions homeassistant/components/switcher_kis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aioswitcher.device import SwitcherBase

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr

Expand All @@ -32,6 +32,8 @@
async def async_setup_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) -> bool:
"""Set up Switcher from a config entry."""

token = entry.data.get(CONF_TOKEN)

@callback
def on_device_data_callback(device: SwitcherBase) -> None:
"""Use as a callback for device data."""
Expand All @@ -45,14 +47,19 @@ def on_device_data_callback(device: SwitcherBase) -> None:

# New device - create device
_LOGGER.info(
"Discovered Switcher device - id: %s, key: %s, name: %s, type: %s (%s)",
"Discovered Switcher device - id: %s, key: %s, name: %s, type: %s (%s), is_token_needed: %s",
device.device_id,
device.device_key,
device.name,
device.device_type.value,
device.device_type.hex_rep,
device.token_needed,
)

if device.token_needed and not token:
entry.async_start_reauth(hass)
return

coordinator = SwitcherDataUpdateCoordinator(hass, entry, device)
coordinator.async_setup()
coordinators[device.device_id] = coordinator
Expand Down
114 changes: 111 additions & 3 deletions homeassistant/components/switcher_kis/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,117 @@

from __future__ import annotations

from homeassistant.helpers import config_entry_flow
from collections.abc import Mapping
import logging
from typing import Any, Final

from aioswitcher.bridge import SwitcherBase
from aioswitcher.device.tools import validate_token
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_TOKEN, CONF_USERNAME

from .const import DOMAIN
from .utils import async_has_devices
from .utils import async_discover_devices

_LOGGER = logging.getLogger(__name__)


CONFIG_SCHEMA: Final = vol.Schema(
{
vol.Required(CONF_USERNAME, default=""): str,
vol.Required(CONF_TOKEN, default=""): str,
}
)


class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle Switcher config flow."""

VERSION = 1

entry: ConfigEntry | None = None
username: str | None = None
token: str | None = None
discovered_devices: dict[str, SwitcherBase] = {}

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the start of the config flow."""
self.discovered_devices = await async_discover_devices()

return self.async_show_form(step_id="confirm")

async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle user-confirmation of the config flow."""
if len(self.discovered_devices) == 0:
return self.async_abort(reason="no_devices_found")

for device_id, device in self.discovered_devices.items():
if device.token_needed:
_LOGGER.debug("Device with ID %s requires a token", device_id)
return await self.async_step_credentials()
return await self._create_entry()

async def async_step_credentials(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the credentials step."""
errors: dict[str, str] = {}
if user_input is not None:
self.username = user_input.get(CONF_USERNAME)
self.token = user_input.get(CONF_TOKEN)

token_is_valid = await validate_token(
user_input[CONF_USERNAME], user_input[CONF_TOKEN]
)
if token_is_valid:
return await self._create_entry()
errors["base"] = "invalid_auth"

return self.async_show_form(
step_id="credentials", data_schema=CONFIG_SCHEMA, errors=errors
)

async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
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
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
assert self.entry is not None

if user_input is not None:
token_is_valid = await validate_token(
user_input[CONF_USERNAME], user_input[CONF_TOKEN]
)
if token_is_valid:
return self.async_update_reload_and_abort(
self.entry, data={**self.entry.data, **user_input}
)
errors["base"] = "invalid_auth"

return self.async_show_form(
step_id="reauth_confirm",
data_schema=CONFIG_SCHEMA,
errors=errors,
)

config_entry_flow.register_discovery_flow(DOMAIN, "Switcher", async_has_devices)
async def _create_entry(self) -> ConfigFlowResult:
return self.async_create_entry(
title="Switcher",
data={
CONF_USERNAME: self.username,
CONF_TOKEN: self.token,
},
)
7 changes: 6 additions & 1 deletion homeassistant/components/switcher_kis/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from aioswitcher.device import SwitcherBase

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, update_coordinator
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand All @@ -23,7 +24,10 @@ class SwitcherDataUpdateCoordinator(
"""Switcher device data update coordinator."""

def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: SwitcherBase
self,
hass: HomeAssistant,
entry: ConfigEntry,
device: SwitcherBase,
) -> None:
"""Initialize the Switcher device coordinator."""
super().__init__(
Expand All @@ -34,6 +38,7 @@ def __init__(
)
self.entry = entry
self.data = device
self.token = entry.data.get(CONF_TOKEN)

async def _async_update_data(self) -> SwitcherBase:
"""Mark device offline if no data."""
Expand Down
25 changes: 18 additions & 7 deletions homeassistant/components/switcher_kis/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ async def async_setup_entry(
@callback
def async_add_cover(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Add cover from Switcher device."""
if coordinator.data.device_type.category == DeviceCategory.SHUTTER:
async_add_entities([SwitcherCoverEntity(coordinator)])
if coordinator.data.device_type.category in (
DeviceCategory.SHUTTER,
DeviceCategory.SINGLE_SHUTTER_DUAL_LIGHT,
):
async_add_entities([SwitcherCoverEntity(coordinator, 0)])

config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_cover)
Expand All @@ -65,9 +68,14 @@ class SwitcherCoverEntity(
| CoverEntityFeature.STOP
)

def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None:
def __init__(
self,
coordinator: SwitcherDataUpdateCoordinator,
cover_id: int | None = None,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._cover_id = cover_id

self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}"
self._attr_device_info = DeviceInfo(
Expand Down Expand Up @@ -102,6 +110,7 @@ async def _async_call_api(self, api: str, *args: Any) -> None:
self.coordinator.data.ip_address,
self.coordinator.data.device_id,
self.coordinator.data.device_key,
self.coordinator.token,
) as swapi:
response = await getattr(swapi, api)(*args)
except (TimeoutError, OSError, RuntimeError) as err:
Expand All @@ -117,16 +126,18 @@ async def _async_call_api(self, api: str, *args: Any) -> None:

async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
await self._async_call_api(API_SET_POSITON, 0)
await self._async_call_api(API_SET_POSITON, 0, self._cover_id)

async def async_open_cover(self, **kwargs: Any) -> None:
"""Open cover."""
await self._async_call_api(API_SET_POSITON, 100)
await self._async_call_api(API_SET_POSITON, 100, self._cover_id)

async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
await self._async_call_api(API_SET_POSITON, kwargs[ATTR_POSITION])
await self._async_call_api(
API_SET_POSITON, kwargs[ATTR_POSITION], self._cover_id
)

async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._async_call_api(API_STOP)
await self._async_call_api(API_STOP, self._cover_id)
20 changes: 19 additions & 1 deletion homeassistant/components/switcher_kis/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@
"step": {
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
},
"credentials": {
"description": "Found a Switcher device that requires a token\nEnter your username and token\nFor more information see https://www.home-assistant.io/integrations/switcher_kis/#prerequisites",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"token": "[%key:common::config_flow::data::access_token%]"
}
},
"reauth_confirm": {
"description": "Found a Switcher device that requires a token\nEnter your username and token\nFor more information see https://www.home-assistant.io/integrations/switcher_kis/#prerequisites",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"token": "[%key:common::config_flow::data::access_token%]"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/switcher_kis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
_LOGGER = logging.getLogger(__name__)


async def async_has_devices(hass: HomeAssistant) -> bool:
async def async_discover_devices() -> dict[str, SwitcherBase]:
"""Discover Switcher devices."""
_LOGGER.debug("Starting discovery")
discovered_devices = {}
Expand All @@ -35,7 +35,7 @@ def on_device_data_callback(device: SwitcherBase) -> None:
await bridge.stop()

_LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices))
return len(discovered_devices) > 0
return discovered_devices


@singleton.singleton("switcher_breeze_remote_manager")
Expand Down
13 changes: 11 additions & 2 deletions tests/components/switcher_kis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
"""Test cases and object for the Switcher integration tests."""

from homeassistant.components.switcher_kis.const import DOMAIN
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry


async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
async def init_integration(
hass: HomeAssistant, username: str | None = None, token: str | None = None
) -> MockConfigEntry:
"""Set up the Switcher integration in Home Assistant."""
entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
data = {}
if username is not None:
data[CONF_USERNAME] = username
if token is not None:
data[CONF_TOKEN] = token

entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN)
entry.add_to_hass(hass)

await hass.config_entries.async_setup(entry.entry_id)
Expand Down
24 changes: 24 additions & 0 deletions tests/components/switcher_kis/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ShutterDirection,
SwitcherPowerPlug,
SwitcherShutter,
SwitcherSingleShutterDualLight,
SwitcherThermostat,
SwitcherWaterHeater,
ThermostatFanLevel,
Expand All @@ -19,29 +20,35 @@
DUMMY_DEVICE_ID2 = "cafe12"
DUMMY_DEVICE_ID3 = "bada77"
DUMMY_DEVICE_ID4 = "bbd164"
DUMMY_DEVICE_ID5 = "bcdb64"
DUMMY_DEVICE_KEY1 = "18"
DUMMY_DEVICE_KEY2 = "01"
DUMMY_DEVICE_KEY3 = "12"
DUMMY_DEVICE_KEY4 = "07"
DUMMY_DEVICE_KEY5 = "15"
DUMMY_DEVICE_NAME1 = "Plug 23BC"
DUMMY_DEVICE_NAME2 = "Heater FE12"
DUMMY_DEVICE_NAME3 = "Breeze AB39"
DUMMY_DEVICE_NAME4 = "Runner DD77"
DUMMY_DEVICE_NAME5 = "RunnerS11 6CF5"
DUMMY_DEVICE_PASSWORD = "12345678"
DUMMY_ELECTRIC_CURRENT1 = 0.5
DUMMY_ELECTRIC_CURRENT2 = 12.8
DUMMY_IP_ADDRESS1 = "192.168.100.157"
DUMMY_IP_ADDRESS2 = "192.168.100.158"
DUMMY_IP_ADDRESS3 = "192.168.100.159"
DUMMY_IP_ADDRESS4 = "192.168.100.160"
DUMMY_IP_ADDRESS5 = "192.168.100.161"
DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8"
DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9"
DUMMY_MAC_ADDRESS3 = "A1:B2:C3:45:67:DA"
DUMMY_MAC_ADDRESS4 = "A1:B2:C3:45:67:DB"
DUMMY_MAC_ADDRESS5 = "A1:B2:C3:45:67:DC"
DUMMY_TOKEN_NEEDED1 = False
DUMMY_TOKEN_NEEDED2 = False
DUMMY_TOKEN_NEEDED3 = False
DUMMY_TOKEN_NEEDED4 = False
DUMMY_TOKEN_NEEDED5 = True
DUMMY_PHONE_ID = "1234"
DUMMY_POWER_CONSUMPTION1 = 100
DUMMY_POWER_CONSUMPTION2 = 2780
Expand All @@ -55,6 +62,9 @@
DUMMY_REMOTE_ID = "ELEC7001"
DUMMY_POSITION = 54
DUMMY_DIRECTION = ShutterDirection.SHUTTER_STOP
DUMMY_USERNAME = "email"
DUMMY_TOKEN = "zvVvd7JxtN7CgvkD1Psujw=="
DUMMY_LIGHTS = [DeviceState.ON, DeviceState.ON]

DUMMY_PLUG_DEVICE = SwitcherPowerPlug(
DeviceType.POWER_PLUG,
Expand Down Expand Up @@ -97,6 +107,20 @@
DUMMY_DIRECTION,
)

DUMMY_SINGLE_SHUTTER_DUAL_LIGHT_DEVICE = SwitcherSingleShutterDualLight(
DeviceType.RUNNER_S11,
DeviceState.ON,
DUMMY_DEVICE_ID5,
DUMMY_DEVICE_KEY5,
DUMMY_IP_ADDRESS5,
DUMMY_MAC_ADDRESS5,
DUMMY_DEVICE_NAME5,
DUMMY_TOKEN_NEEDED5,
DUMMY_POSITION,
DUMMY_DIRECTION,
DUMMY_LIGHTS,
)

DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat(
DeviceType.BREEZE,
DeviceState.ON,
Expand Down
Loading