mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Add Switcher config flow discovery support (#52316)
This commit is contained in:
parent
51d16202ab
commit
ea6e325762
@ -992,8 +992,6 @@ omit =
|
|||||||
homeassistant/components/swiss_public_transport/sensor.py
|
homeassistant/components/swiss_public_transport/sensor.py
|
||||||
homeassistant/components/swisscom/device_tracker.py
|
homeassistant/components/swisscom/device_tracker.py
|
||||||
homeassistant/components/switchbot/switch.py
|
homeassistant/components/switchbot/switch.py
|
||||||
homeassistant/components/switcher_kis/sensor.py
|
|
||||||
homeassistant/components/switcher_kis/switch.py
|
|
||||||
homeassistant/components/switchmate/switch.py
|
homeassistant/components/switchmate/switch.py
|
||||||
homeassistant/components/syncthing/__init__.py
|
homeassistant/components/syncthing/__init__.py
|
||||||
homeassistant/components/syncthing/sensor.py
|
homeassistant/components/syncthing/sensor.py
|
||||||
|
@ -79,6 +79,7 @@ homeassistant.components.ssdp.*
|
|||||||
homeassistant.components.stream.*
|
homeassistant.components.stream.*
|
||||||
homeassistant.components.sun.*
|
homeassistant.components.sun.*
|
||||||
homeassistant.components.switch.*
|
homeassistant.components.switch.*
|
||||||
|
homeassistant.components.switcher_kis.*
|
||||||
homeassistant.components.synology_dsm.*
|
homeassistant.components.synology_dsm.*
|
||||||
homeassistant.components.systemmonitor.*
|
homeassistant.components.systemmonitor.*
|
||||||
homeassistant.components.tag.*
|
homeassistant.components.tag.*
|
||||||
|
@ -1,85 +1,181 @@
|
|||||||
"""The Switcher integration."""
|
"""The Switcher integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for
|
from datetime import timedelta
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioswitcher.bridge import SwitcherV2Bridge
|
from aioswitcher.device import SwitcherBase
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import (
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
config_validation as cv,
|
||||||
|
device_registry,
|
||||||
|
update_coordinator,
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
from homeassistant.helpers.typing import EventType
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_DEVICE_PASSWORD,
|
CONF_DEVICE_PASSWORD,
|
||||||
CONF_PHONE_ID,
|
CONF_PHONE_ID,
|
||||||
DATA_DEVICE,
|
DATA_DEVICE,
|
||||||
|
DATA_DISCOVERY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SIGNAL_SWITCHER_DEVICE_UPDATE,
|
MAX_UPDATE_INTERVAL_SEC,
|
||||||
|
SIGNAL_DEVICE_ADD,
|
||||||
)
|
)
|
||||||
|
from .utils import async_start_bridge, async_stop_bridge
|
||||||
|
|
||||||
|
PLATFORMS = ["switch", "sensor"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CCONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
vol.All(
|
||||||
DOMAIN: vol.Schema(
|
cv.deprecated(DOMAIN),
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PHONE_ID): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
{
|
||||||
vol.Required(CONF_DEVICE_PASSWORD): cv.string,
|
vol.Required(CONF_PHONE_ID): cv.string,
|
||||||
}
|
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||||
)
|
vol.Required(CONF_DEVICE_PASSWORD): cv.string,
|
||||||
},
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
"""Set up the switcher component."""
|
"""Set up the switcher component."""
|
||||||
phone_id = config[DOMAIN][CONF_PHONE_ID]
|
hass.data.setdefault(DOMAIN, {})
|
||||||
device_id = config[DOMAIN][CONF_DEVICE_ID]
|
|
||||||
device_password = config[DOMAIN][CONF_DEVICE_PASSWORD]
|
|
||||||
|
|
||||||
v2bridge = SwitcherV2Bridge(hass.loop, phone_id, device_id, device_password)
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
await v2bridge.start()
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
async def async_stop_bridge(event: EventType) -> None:
|
|
||||||
"""On Home Assistant stop, gracefully stop the bridge if running."""
|
|
||||||
await v2bridge.stop()
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge)
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Switcher from a config entry."""
|
||||||
try:
|
hass.data[DOMAIN][DATA_DEVICE] = {}
|
||||||
device_data = await wait_for(v2bridge.queue.get(), timeout=10.0)
|
|
||||||
except (Asyncio_TimeoutError, RuntimeError):
|
|
||||||
_LOGGER.exception("Failed to get response from device")
|
|
||||||
await v2bridge.stop()
|
|
||||||
return False
|
|
||||||
hass.data[DOMAIN] = {DATA_DEVICE: device_data}
|
|
||||||
|
|
||||||
hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config))
|
|
||||||
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def device_updates(timestamp: datetime | None) -> None:
|
def on_device_data_callback(device: SwitcherBase) -> None:
|
||||||
"""Use for updating the device data from the queue."""
|
"""Use as a callback for device data."""
|
||||||
if v2bridge.running:
|
|
||||||
try:
|
|
||||||
device_new_data = v2bridge.queue.get_nowait()
|
|
||||||
if device_new_data:
|
|
||||||
async_dispatcher_send(
|
|
||||||
hass, SIGNAL_SWITCHER_DEVICE_UPDATE, device_new_data
|
|
||||||
)
|
|
||||||
except QueueEmpty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async_track_time_interval(hass, device_updates, timedelta(seconds=4))
|
# Existing device update device data
|
||||||
|
if device.device_id in hass.data[DOMAIN][DATA_DEVICE]:
|
||||||
|
wrapper: SwitcherDeviceWrapper = hass.data[DOMAIN][DATA_DEVICE][
|
||||||
|
device.device_id
|
||||||
|
]
|
||||||
|
wrapper.async_set_updated_data(device)
|
||||||
|
return
|
||||||
|
|
||||||
|
# New device - create device
|
||||||
|
_LOGGER.info(
|
||||||
|
"Discovered Switcher device - id: %s, name: %s, type: %s (%s)",
|
||||||
|
device.device_id,
|
||||||
|
device.name,
|
||||||
|
device.device_type.value,
|
||||||
|
device.device_type.hex_rep,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrapper = hass.data[DOMAIN][DATA_DEVICE][
|
||||||
|
device.device_id
|
||||||
|
] = SwitcherDeviceWrapper(hass, entry, device)
|
||||||
|
hass.async_create_task(wrapper.async_setup())
|
||||||
|
|
||||||
|
async def platforms_setup_task() -> None:
|
||||||
|
# Must be ready before dispatcher is called
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
await hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||||
|
|
||||||
|
discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None)
|
||||||
|
if discovery_task is not None:
|
||||||
|
discovered_devices = await discovery_task
|
||||||
|
for device in discovered_devices.values():
|
||||||
|
on_device_data_callback(device)
|
||||||
|
|
||||||
|
await async_start_bridge(hass, on_device_data_callback)
|
||||||
|
|
||||||
|
hass.async_create_task(platforms_setup_task())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def stop_bridge(event: Event) -> None:
|
||||||
|
await async_stop_bridge(hass)
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SwitcherDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
||||||
|
"""Wrapper for a Switcher device with Home Assistant specific functions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, entry: ConfigEntry, device: SwitcherBase
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Switcher device wrapper."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=device.name,
|
||||||
|
update_interval=timedelta(seconds=MAX_UPDATE_INTERVAL_SEC),
|
||||||
|
)
|
||||||
|
self.hass = hass
|
||||||
|
self.entry = entry
|
||||||
|
self.data = device
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Mark device offline if no data."""
|
||||||
|
raise update_coordinator.UpdateFailed(
|
||||||
|
f"Device {self.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
"""Switcher device model."""
|
||||||
|
return self.data.device_type.value # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_id(self) -> str:
|
||||||
|
"""Switcher device id."""
|
||||||
|
return self.data.device_id # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac_address(self) -> str:
|
||||||
|
"""Switcher device mac address."""
|
||||||
|
return self.data.mac_address # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Set up the wrapper."""
|
||||||
|
dev_reg = await device_registry.async_get_registry(self.hass)
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=self.entry.entry_id,
|
||||||
|
connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac_address)},
|
||||||
|
identifiers={(DOMAIN, self.device_id)},
|
||||||
|
manufacturer="Switcher",
|
||||||
|
name=self.name,
|
||||||
|
model=self.model,
|
||||||
|
)
|
||||||
|
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADD, self)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
await async_stop_bridge(hass)
|
||||||
|
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(DATA_DEVICE)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
49
homeassistant/components/switcher_kis/config_flow.py
Normal file
49
homeassistant/components/switcher_kis/config_flow.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""Config flow for Switcher integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DATA_DISCOVERY, DOMAIN
|
||||||
|
from .utils import async_discover_devices
|
||||||
|
|
||||||
|
|
||||||
|
class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle Switcher config flow."""
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config: ConfigType) -> FlowResult:
|
||||||
|
"""Handle a flow initiated by import."""
|
||||||
|
if self._async_current_entries(True):
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
return self.async_create_entry(title="Switcher", data={})
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the start of the config flow."""
|
||||||
|
if self._async_current_entries(True):
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
self.hass.data.setdefault(DOMAIN, {})
|
||||||
|
if DATA_DISCOVERY not in self.hass.data[DOMAIN]:
|
||||||
|
self.hass.data[DOMAIN][DATA_DISCOVERY] = self.hass.async_create_task(
|
||||||
|
async_discover_devices()
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="confirm")
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle user-confirmation of the config flow."""
|
||||||
|
discovered_devices = await self.hass.data[DOMAIN][DATA_DISCOVERY]
|
||||||
|
|
||||||
|
if len(discovered_devices) == 0:
|
||||||
|
self.hass.data[DOMAIN].pop(DATA_DISCOVERY)
|
||||||
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
|
||||||
|
return self.async_create_entry(title="Switcher", data={})
|
@ -1,20 +1,22 @@
|
|||||||
"""Constants for the Switcher integration."""
|
"""Constants for the Switcher integration."""
|
||||||
|
|
||||||
DOMAIN = "switcher_kis"
|
DOMAIN = "switcher_kis"
|
||||||
|
|
||||||
CONF_DEVICE_PASSWORD = "device_password"
|
CONF_DEVICE_PASSWORD = "device_password"
|
||||||
CONF_PHONE_ID = "phone_id"
|
CONF_PHONE_ID = "phone_id"
|
||||||
|
|
||||||
|
DATA_BRIDGE = "bridge"
|
||||||
DATA_DEVICE = "device"
|
DATA_DEVICE = "device"
|
||||||
|
DATA_DISCOVERY = "discovery"
|
||||||
|
|
||||||
SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update"
|
DISCOVERY_TIME_SEC = 6
|
||||||
|
|
||||||
ATTR_AUTO_OFF_SET = "auto_off_set"
|
SIGNAL_DEVICE_ADD = "switcher_device_add"
|
||||||
ATTR_ELECTRIC_CURRENT = "electric_current"
|
|
||||||
ATTR_REMAINING_TIME = "remaining_time"
|
|
||||||
|
|
||||||
|
# Services
|
||||||
CONF_AUTO_OFF = "auto_off"
|
CONF_AUTO_OFF = "auto_off"
|
||||||
CONF_TIMER_MINUTES = "timer_minutes"
|
CONF_TIMER_MINUTES = "timer_minutes"
|
||||||
|
|
||||||
SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
|
SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer"
|
SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer"
|
||||||
|
|
||||||
|
# Defines the maximum interval device must send an update before it marked unavailable
|
||||||
|
MAX_UPDATE_INTERVAL_SEC = 20
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"name": "Switcher",
|
"name": "Switcher",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
|
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
|
||||||
"codeowners": ["@tomerfi","@thecode"],
|
"codeowners": ["@tomerfi","@thecode"],
|
||||||
"requirements": ["aioswitcher==1.2.3"],
|
"requirements": ["aioswitcher==2.0.4"],
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push",
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from aioswitcher.consts import WAITING_TEXT
|
from aioswitcher.device import DeviceCategory
|
||||||
from aioswitcher.devices import SwitcherV2Device
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
DEVICE_CLASS_CURRENT,
|
DEVICE_CLASS_CURRENT,
|
||||||
@ -12,13 +11,17 @@ from homeassistant.components.sensor import (
|
|||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT
|
from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import device_registry
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import DiscoveryInfoType, StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE
|
from . import SwitcherDeviceWrapper
|
||||||
|
from .const import SIGNAL_DEVICE_ADD
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -31,7 +34,6 @@ class AttributeDescription:
|
|||||||
device_class: str | None = None
|
device_class: str | None = None
|
||||||
state_class: str | None = None
|
state_class: str | None = None
|
||||||
default_enabled: bool = True
|
default_enabled: bool = True
|
||||||
default_value: float | int | str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
POWER_SENSORS = {
|
POWER_SENSORS = {
|
||||||
@ -40,14 +42,12 @@ POWER_SENSORS = {
|
|||||||
unit=POWER_WATT,
|
unit=POWER_WATT,
|
||||||
device_class=DEVICE_CLASS_POWER,
|
device_class=DEVICE_CLASS_POWER,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
default_value=0,
|
|
||||||
),
|
),
|
||||||
"electric_current": AttributeDescription(
|
"electric_current": AttributeDescription(
|
||||||
name="Electric Current",
|
name="Electric Current",
|
||||||
unit=ELECTRICAL_CURRENT_AMPERE,
|
unit=ELECTRICAL_CURRENT_AMPERE,
|
||||||
device_class=DEVICE_CLASS_CURRENT,
|
device_class=DEVICE_CLASS_CURRENT,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
default_value=0.0,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,77 +55,73 @@ TIME_SENSORS = {
|
|||||||
"remaining_time": AttributeDescription(
|
"remaining_time": AttributeDescription(
|
||||||
name="Remaining Time",
|
name="Remaining Time",
|
||||||
icon="mdi:av-timer",
|
icon="mdi:av-timer",
|
||||||
default_value="00:00:00",
|
|
||||||
),
|
),
|
||||||
"auto_off_set": AttributeDescription(
|
"auto_off_set": AttributeDescription(
|
||||||
name="Auto Shutdown",
|
name="Auto Shutdown",
|
||||||
icon="mdi:progress-clock",
|
icon="mdi:progress-clock",
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
default_value="00:00:00",
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
SENSORS = {**POWER_SENSORS, **TIME_SENSORS}
|
POWER_PLUG_SENSORS = POWER_SENSORS
|
||||||
|
WATER_HEATER_SENSORS = {**POWER_SENSORS, **TIME_SENSORS}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: dict,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Switcher sensor from config entry."""
|
"""Set up Switcher sensor from config entry."""
|
||||||
device_data = hass.data[DOMAIN][DATA_DEVICE]
|
|
||||||
|
|
||||||
async_add_entities(
|
@callback
|
||||||
SwitcherSensorEntity(device_data, attribute, sensor)
|
def async_add_sensors(wrapper: SwitcherDeviceWrapper) -> None:
|
||||||
for attribute, sensor in SENSORS.items()
|
"""Add sensors from Switcher device."""
|
||||||
|
if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG:
|
||||||
|
async_add_entities(
|
||||||
|
SwitcherSensorEntity(wrapper, attribute, info)
|
||||||
|
for attribute, info in POWER_PLUG_SENSORS.items()
|
||||||
|
)
|
||||||
|
elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER:
|
||||||
|
async_add_entities(
|
||||||
|
SwitcherSensorEntity(wrapper, attribute, info)
|
||||||
|
for attribute, info in WATER_HEATER_SENSORS.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.async_on_unload(
|
||||||
|
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_sensors)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SwitcherSensorEntity(SensorEntity):
|
class SwitcherSensorEntity(CoordinatorEntity, SensorEntity):
|
||||||
"""Representation of a Switcher sensor entity."""
|
"""Representation of a Switcher sensor entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_data: SwitcherV2Device,
|
wrapper: SwitcherDeviceWrapper,
|
||||||
attribute: str,
|
attribute: str,
|
||||||
description: AttributeDescription,
|
description: AttributeDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._device_data = device_data
|
super().__init__(wrapper)
|
||||||
|
self.wrapper = wrapper
|
||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
self.description = description
|
|
||||||
|
|
||||||
# Entity class attributes
|
# Entity class attributes
|
||||||
self._attr_name = f"{self._device_data.name} {self.description.name}"
|
self._attr_name = f"{wrapper.name} {description.name}"
|
||||||
self._attr_icon = self.description.icon
|
self._attr_icon = description.icon
|
||||||
self._attr_unit_of_measurement = self.description.unit
|
self._attr_unit_of_measurement = description.unit
|
||||||
self._attr_device_class = self.description.device_class
|
self._attr_device_class = description.device_class
|
||||||
self._attr_entity_registry_enabled_default = self.description.default_enabled
|
self._attr_entity_registry_enabled_default = description.default_enabled
|
||||||
self._attr_should_poll = False
|
|
||||||
|
|
||||||
self._attr_unique_id = f"{self._device_data.device_id}-{self._device_data.mac_addr}-{self.attribute}"
|
self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}-{attribute}"
|
||||||
|
self._attr_device_info = {
|
||||||
|
"connections": {
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> StateType:
|
def state(self) -> StateType:
|
||||||
"""Return value of sensor."""
|
"""Return value of sensor."""
|
||||||
value = getattr(self._device_data, self.attribute)
|
return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return]
|
||||||
if value and value is not WAITING_TEXT:
|
|
||||||
return value
|
|
||||||
|
|
||||||
return self.description.default_value
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Run when entity about to be added to hass."""
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_data(self, device_data: SwitcherV2Device) -> None:
|
|
||||||
"""Update the entity data."""
|
|
||||||
self._device_data = device_data
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
@ -5,6 +5,7 @@ set_auto_off:
|
|||||||
entity:
|
entity:
|
||||||
integration: switcher_kis
|
integration: switcher_kis
|
||||||
domain: switch
|
domain: switch
|
||||||
|
device_class: switch
|
||||||
fields:
|
fields:
|
||||||
auto_off:
|
auto_off:
|
||||||
name: Auto off
|
name: Auto off
|
||||||
@ -21,6 +22,7 @@ turn_on_with_timer:
|
|||||||
entity:
|
entity:
|
||||||
integration: switcher_kis
|
integration: switcher_kis
|
||||||
domain: switch
|
domain: switch
|
||||||
|
device_class: switch
|
||||||
fields:
|
fields:
|
||||||
timer_minutes:
|
timer_minutes:
|
||||||
name: Timer
|
name: Timer
|
||||||
|
13
homeassistant/components/switcher_kis/strings.json
Normal file
13
homeassistant/components/switcher_kis/strings.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +1,42 @@
|
|||||||
"""Home Assistant Switcher Component Switch platform."""
|
"""Switcher integration Switch platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aioswitcher.api import SwitcherV2Api
|
import asyncio
|
||||||
from aioswitcher.api.messages import SwitcherV2ControlResponseMSG
|
from datetime import timedelta
|
||||||
from aioswitcher.consts import (
|
import logging
|
||||||
COMMAND_OFF,
|
from typing import Any
|
||||||
COMMAND_ON,
|
|
||||||
STATE_OFF as SWITCHER_STATE_OFF,
|
from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse
|
||||||
STATE_ON as SWITCHER_STATE_ON,
|
from aioswitcher.device import DeviceCategory, DeviceState
|
||||||
)
|
|
||||||
from aioswitcher.devices import SwitcherV2Device
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import (
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
DEVICE_CLASS_OUTLET,
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
DEVICE_CLASS_SWITCH,
|
||||||
|
SwitchEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
device_registry,
|
||||||
|
entity_platform,
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import SwitcherDeviceWrapper
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_AUTO_OFF,
|
CONF_AUTO_OFF,
|
||||||
CONF_TIMER_MINUTES,
|
CONF_TIMER_MINUTES,
|
||||||
DATA_DEVICE,
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
SIGNAL_SWITCHER_DEVICE_UPDATE,
|
SIGNAL_DEVICE_ADD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVICE_SET_AUTO_OFF_SCHEMA = {
|
SERVICE_SET_AUTO_OFF_SCHEMA = {
|
||||||
vol.Required(CONF_AUTO_OFF): cv.time_period_str,
|
vol.Required(CONF_AUTO_OFF): cv.time_period_str,
|
||||||
}
|
}
|
||||||
@ -39,135 +48,142 @@ SERVICE_TURN_ON_WITH_TIMER_SCHEMA = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: dict,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: dict,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the switcher platform for the switch component."""
|
"""Set up Switcher switch from config entry."""
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async def async_set_auto_off_service(entity, service_call: ServiceCall) -> None:
|
|
||||||
"""Use for handling setting device auto-off service calls."""
|
|
||||||
async with SwitcherV2Api(
|
|
||||||
hass.loop,
|
|
||||||
device_data.ip_addr,
|
|
||||||
device_data.phone_id,
|
|
||||||
device_data.device_id,
|
|
||||||
device_data.device_password,
|
|
||||||
) as swapi:
|
|
||||||
await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF])
|
|
||||||
|
|
||||||
async def async_turn_on_with_timer_service(
|
|
||||||
entity, service_call: ServiceCall
|
|
||||||
) -> None:
|
|
||||||
"""Use for handling turning device on with a timer service calls."""
|
|
||||||
async with SwitcherV2Api(
|
|
||||||
hass.loop,
|
|
||||||
device_data.ip_addr,
|
|
||||||
device_data.phone_id,
|
|
||||||
device_data.device_id,
|
|
||||||
device_data.device_password,
|
|
||||||
) as swapi:
|
|
||||||
await swapi.control_device(
|
|
||||||
COMMAND_ON, service_call.data[CONF_TIMER_MINUTES]
|
|
||||||
)
|
|
||||||
|
|
||||||
device_data = hass.data[DOMAIN][DATA_DEVICE]
|
|
||||||
async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])])
|
|
||||||
|
|
||||||
platform = entity_platform.async_get_current_platform()
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
SERVICE_SET_AUTO_OFF_SCHEMA,
|
SERVICE_SET_AUTO_OFF_SCHEMA,
|
||||||
async_set_auto_off_service,
|
"async_set_auto_off_service",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
SERVICE_TURN_ON_WITH_TIMER_SCHEMA,
|
SERVICE_TURN_ON_WITH_TIMER_SCHEMA,
|
||||||
async_turn_on_with_timer_service,
|
"async_turn_on_with_timer_service",
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_switch(wrapper: SwitcherDeviceWrapper) -> None:
|
||||||
|
"""Add switch from Switcher device."""
|
||||||
|
if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG:
|
||||||
|
async_add_entities([SwitcherPowerPlugSwitchEntity(wrapper)])
|
||||||
|
elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER:
|
||||||
|
async_add_entities([SwitcherWaterHeaterSwitchEntity(wrapper)])
|
||||||
|
|
||||||
|
config_entry.async_on_unload(
|
||||||
|
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_switch)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SwitcherControl(SwitchEntity):
|
class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity):
|
||||||
"""Home Assistant switch entity."""
|
"""Representation of a Switcher switch entity."""
|
||||||
|
|
||||||
def __init__(self, device_data: SwitcherV2Device) -> None:
|
def __init__(self, wrapper: SwitcherDeviceWrapper) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._self_initiated = False
|
super().__init__(wrapper)
|
||||||
self._device_data = device_data
|
self.wrapper = wrapper
|
||||||
self._state = device_data.state
|
self.control_result: bool | None = None
|
||||||
|
|
||||||
@property
|
# Entity class attributes
|
||||||
def name(self) -> str:
|
self._attr_name = wrapper.name
|
||||||
"""Return the device's name."""
|
self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}"
|
||||||
return self._device_data.name
|
self._attr_device_info = {
|
||||||
|
"connections": {
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@callback
|
||||||
def should_poll(self) -> bool:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Return False, entity pushes its state to HA."""
|
"""When device updates, clear control result that overrides state."""
|
||||||
return False
|
self.control_result = None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@property
|
async def _async_call_api(self, api: str, *args: Any) -> None:
|
||||||
def unique_id(self) -> str:
|
"""Call Switcher API."""
|
||||||
"""Return a unique ID."""
|
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
|
||||||
return f"{self._device_data.device_id}-{self._device_data.mac_addr}"
|
response: SwitcherBaseResponse = None
|
||||||
|
error = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with SwitcherApi(
|
||||||
|
self.wrapper.data.ip_address, self.wrapper.device_id
|
||||||
|
) as swapi:
|
||||||
|
response = await getattr(swapi, api)(*args)
|
||||||
|
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||||
|
error = repr(err)
|
||||||
|
|
||||||
|
if error or not response or not response.successful:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Call api for %s failed, api: '%s', args: %s, response/error: %s",
|
||||||
|
self.name,
|
||||||
|
api,
|
||||||
|
args,
|
||||||
|
response or error,
|
||||||
|
)
|
||||||
|
self.wrapper.last_update_success = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if entity is on."""
|
"""Return True if entity is on."""
|
||||||
return self._state == SWITCHER_STATE_ON
|
if self.control_result is not None:
|
||||||
|
return self.control_result
|
||||||
|
|
||||||
@property
|
return bool(self.wrapper.data.device_state == DeviceState.ON)
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return True if entity is available."""
|
|
||||||
return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF]
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Run when entity about to be added to hass."""
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_data(self, device_data: SwitcherV2Device) -> None:
|
|
||||||
"""Update the entity data."""
|
|
||||||
if self._self_initiated:
|
|
||||||
self._self_initiated = False
|
|
||||||
else:
|
|
||||||
self._device_data = device_data
|
|
||||||
self._state = self._device_data.state
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: dict) -> None:
|
async def async_turn_on(self, **kwargs: dict) -> None:
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
await self._control_device(True)
|
await self._async_call_api("control_device", Command.ON)
|
||||||
|
self.control_result = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: dict) -> None:
|
async def async_turn_off(self, **kwargs: dict) -> None:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
await self._control_device(False)
|
await self._async_call_api("control_device", Command.OFF)
|
||||||
|
self.control_result = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def _control_device(self, send_on: bool) -> None:
|
async def async_set_auto_off_service(self, auto_off: timedelta) -> None:
|
||||||
"""Turn the entity on or off."""
|
"""Use for handling setting device auto-off service calls."""
|
||||||
response: SwitcherV2ControlResponseMSG = None
|
_LOGGER.warning(
|
||||||
async with SwitcherV2Api(
|
"Service '%s' is not supported by %s",
|
||||||
self.hass.loop,
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
self._device_data.ip_addr,
|
self.name,
|
||||||
self._device_data.phone_id,
|
)
|
||||||
self._device_data.device_id,
|
|
||||||
self._device_data.device_password,
|
|
||||||
) as swapi:
|
|
||||||
response = await swapi.control_device(
|
|
||||||
COMMAND_ON if send_on else COMMAND_OFF
|
|
||||||
)
|
|
||||||
|
|
||||||
if response and response.successful:
|
async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None:
|
||||||
self._self_initiated = True
|
"""Use for turning device on with a timer service calls."""
|
||||||
self._state = SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF
|
_LOGGER.warning(
|
||||||
self.async_write_ha_state()
|
"Service '%s' is not supported by %s",
|
||||||
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SwitcherPowerPlugSwitchEntity(SwitcherBaseSwitchEntity):
|
||||||
|
"""Representation of a Switcher power plug switch entity."""
|
||||||
|
|
||||||
|
_attr_device_class = DEVICE_CLASS_OUTLET
|
||||||
|
|
||||||
|
|
||||||
|
class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity):
|
||||||
|
"""Representation of a Switcher water heater switch entity."""
|
||||||
|
|
||||||
|
_attr_device_class = DEVICE_CLASS_SWITCH
|
||||||
|
|
||||||
|
async def async_set_auto_off_service(self, auto_off: timedelta) -> None:
|
||||||
|
"""Use for handling setting device auto-off service calls."""
|
||||||
|
await self._async_call_api("set_auto_shutdown", auto_off)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None:
|
||||||
|
"""Use for turning device on with a timer service calls."""
|
||||||
|
await self._async_call_api("control_device", Command.ON, timer_minutes)
|
||||||
|
self.control_result = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
13
homeassistant/components/switcher_kis/translations/en.json
Normal file
13
homeassistant/components/switcher_kis/translations/en.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"no_devices_found": "No devices found on the network",
|
||||||
|
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Do you want to start set up?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
homeassistant/components/switcher_kis/utils.py
Normal file
54
homeassistant/components/switcher_kis/utils.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""Switcher integration helpers functions."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from aioswitcher.bridge import SwitcherBase, SwitcherBridge
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_start_bridge(
|
||||||
|
hass: HomeAssistant, on_device_callback: Callable[[SwitcherBase], Any]
|
||||||
|
) -> None:
|
||||||
|
"""Start switcher UDP bridge."""
|
||||||
|
bridge = hass.data[DOMAIN][DATA_BRIDGE] = SwitcherBridge(on_device_callback)
|
||||||
|
_LOGGER.debug("Starting Switcher bridge")
|
||||||
|
await bridge.start()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_stop_bridge(hass: HomeAssistant) -> None:
|
||||||
|
"""Stop switcher UDP bridge."""
|
||||||
|
bridge: SwitcherBridge = hass.data[DOMAIN].get(DATA_BRIDGE)
|
||||||
|
if bridge is not None:
|
||||||
|
_LOGGER.debug("Stopping Switcher bridge")
|
||||||
|
await bridge.stop()
|
||||||
|
hass.data[DOMAIN].pop(DATA_BRIDGE)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_discover_devices() -> dict[str, SwitcherBase]:
|
||||||
|
"""Discover Switcher devices."""
|
||||||
|
_LOGGER.debug("Starting discovery")
|
||||||
|
discovered_devices = {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def on_device_data_callback(device: SwitcherBase) -> None:
|
||||||
|
"""Use as a callback for device data."""
|
||||||
|
if device.device_id in discovered_devices:
|
||||||
|
return
|
||||||
|
|
||||||
|
discovered_devices[device.device_id] = device
|
||||||
|
|
||||||
|
bridge = SwitcherBridge(on_device_data_callback)
|
||||||
|
await bridge.start()
|
||||||
|
await asyncio.sleep(DISCOVERY_TIME_SEC)
|
||||||
|
await bridge.stop()
|
||||||
|
|
||||||
|
_LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices))
|
||||||
|
return discovered_devices
|
@ -251,6 +251,7 @@ FLOWS = [
|
|||||||
"srp_energy",
|
"srp_energy",
|
||||||
"starline",
|
"starline",
|
||||||
"subaru",
|
"subaru",
|
||||||
|
"switcher_kis",
|
||||||
"syncthing",
|
"syncthing",
|
||||||
"syncthru",
|
"syncthru",
|
||||||
"synology_dsm",
|
"synology_dsm",
|
||||||
|
14
mypy.ini
14
mypy.ini
@ -880,6 +880,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.switcher_kis.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.synology_dsm.*]
|
[mypy-homeassistant.components.synology_dsm.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
@ -1539,9 +1550,6 @@ ignore_errors = true
|
|||||||
[mypy-homeassistant.components.switchbot.*]
|
[mypy-homeassistant.components.switchbot.*]
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.switcher_kis.*]
|
|
||||||
ignore_errors = true
|
|
||||||
|
|
||||||
[mypy-homeassistant.components.synology_srm.*]
|
[mypy-homeassistant.components.synology_srm.*]
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ aiorecollect==1.0.5
|
|||||||
aioshelly==0.6.4
|
aioshelly==0.6.4
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==1.2.3
|
aioswitcher==2.0.4
|
||||||
|
|
||||||
# homeassistant.components.syncthing
|
# homeassistant.components.syncthing
|
||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
|
@ -158,7 +158,7 @@ aiorecollect==1.0.5
|
|||||||
aioshelly==0.6.4
|
aioshelly==0.6.4
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==1.2.3
|
aioswitcher==2.0.4
|
||||||
|
|
||||||
# homeassistant.components.syncthing
|
# homeassistant.components.syncthing
|
||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
|
@ -175,7 +175,6 @@ IGNORED_MODULES: Final[list[str]] = [
|
|||||||
"homeassistant.components.stt.*",
|
"homeassistant.components.stt.*",
|
||||||
"homeassistant.components.surepetcare.*",
|
"homeassistant.components.surepetcare.*",
|
||||||
"homeassistant.components.switchbot.*",
|
"homeassistant.components.switchbot.*",
|
||||||
"homeassistant.components.switcher_kis.*",
|
|
||||||
"homeassistant.components.synology_srm.*",
|
"homeassistant.components.synology_srm.*",
|
||||||
"homeassistant.components.system_health.*",
|
"homeassistant.components.system_health.*",
|
||||||
"homeassistant.components.system_log.*",
|
"homeassistant.components.system_log.*",
|
||||||
|
@ -1 +1,16 @@
|
|||||||
"""Test cases and object for the Switcher integration tests."""
|
"""Test cases and object for the Switcher integration tests."""
|
||||||
|
from homeassistant.components.switcher_kis.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
|
||||||
|
"""Set up the Switcher integration in Home Assistant."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return entry
|
||||||
|
@ -1,194 +1,61 @@
|
|||||||
"""Common fixtures and objects for the Switcher integration tests."""
|
"""Common fixtures and objects for the Switcher integration tests."""
|
||||||
from __future__ import annotations
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
from asyncio import Queue
|
import pytest
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Generator
|
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
from pytest import fixture
|
|
||||||
|
|
||||||
from .consts import (
|
|
||||||
DUMMY_AUTO_OFF_SET,
|
|
||||||
DUMMY_DEVICE_ID,
|
|
||||||
DUMMY_DEVICE_NAME,
|
|
||||||
DUMMY_DEVICE_PASSWORD,
|
|
||||||
DUMMY_DEVICE_STATE,
|
|
||||||
DUMMY_ELECTRIC_CURRENT,
|
|
||||||
DUMMY_IP_ADDRESS,
|
|
||||||
DUMMY_MAC_ADDRESS,
|
|
||||||
DUMMY_PHONE_ID,
|
|
||||||
DUMMY_POWER_CONSUMPTION,
|
|
||||||
DUMMY_REMAINING_TIME,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@patch("aioswitcher.devices.SwitcherV2Device")
|
@pytest.fixture
|
||||||
class MockSwitcherV2Device:
|
def mock_bridge(request):
|
||||||
"""Class for mocking the aioswitcher.devices.SwitcherV2Device object."""
|
"""Return a mocked SwitcherBridge."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.utils.SwitcherBridge", autospec=True
|
||||||
|
) as bridge_mock:
|
||||||
|
bridge = bridge_mock.return_value
|
||||||
|
|
||||||
def __init__(self) -> None:
|
bridge.devices = []
|
||||||
"""Initialize the object."""
|
if hasattr(request, "param") and request.param:
|
||||||
self._last_state_change = datetime.now()
|
bridge.devices = request.param
|
||||||
|
|
||||||
@property
|
async def start():
|
||||||
def device_id(self) -> str:
|
bridge.is_running = True
|
||||||
"""Return the device id."""
|
|
||||||
return DUMMY_DEVICE_ID
|
|
||||||
|
|
||||||
@property
|
for device in bridge.devices:
|
||||||
def ip_addr(self) -> str:
|
bridge_mock.call_args[0][0](device)
|
||||||
"""Return the ip address."""
|
|
||||||
return DUMMY_IP_ADDRESS
|
|
||||||
|
|
||||||
@property
|
def mock_callbacks(devices):
|
||||||
def mac_addr(self) -> str:
|
for device in devices:
|
||||||
"""Return the mac address."""
|
bridge_mock.call_args[0][0](device)
|
||||||
return DUMMY_MAC_ADDRESS
|
|
||||||
|
|
||||||
@property
|
async def stop():
|
||||||
def name(self) -> str:
|
bridge.is_running = False
|
||||||
"""Return the device name."""
|
|
||||||
return DUMMY_DEVICE_NAME
|
|
||||||
|
|
||||||
@property
|
bridge.start = AsyncMock(side_effect=start)
|
||||||
def state(self) -> str:
|
bridge.mock_callbacks = Mock(side_effect=mock_callbacks)
|
||||||
"""Return the device state."""
|
bridge.stop = AsyncMock(side_effect=stop)
|
||||||
return DUMMY_DEVICE_STATE
|
|
||||||
|
|
||||||
@property
|
yield bridge
|
||||||
def remaining_time(self) -> str | None:
|
|
||||||
"""Return the time left to auto-off."""
|
|
||||||
return DUMMY_REMAINING_TIME
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auto_off_set(self) -> str:
|
|
||||||
"""Return the auto-off configuration value."""
|
|
||||||
return DUMMY_AUTO_OFF_SET
|
|
||||||
|
|
||||||
@property
|
|
||||||
def power_consumption(self) -> int:
|
|
||||||
"""Return the power consumption in watts."""
|
|
||||||
return DUMMY_POWER_CONSUMPTION
|
|
||||||
|
|
||||||
@property
|
|
||||||
def electric_current(self) -> float:
|
|
||||||
"""Return the power consumption in amps."""
|
|
||||||
return DUMMY_ELECTRIC_CURRENT
|
|
||||||
|
|
||||||
@property
|
|
||||||
def phone_id(self) -> str:
|
|
||||||
"""Return the phone id."""
|
|
||||||
return DUMMY_PHONE_ID
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_password(self) -> str:
|
|
||||||
"""Return the device password."""
|
|
||||||
return DUMMY_DEVICE_PASSWORD
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_data_update(self) -> datetime:
|
|
||||||
"""Return the timestamp of the last update."""
|
|
||||||
return datetime.now()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_state_change(self) -> datetime:
|
|
||||||
"""Return the timestamp of the state change."""
|
|
||||||
return self._last_state_change
|
|
||||||
|
|
||||||
|
|
||||||
@fixture(name="mock_bridge")
|
@pytest.fixture
|
||||||
def mock_bridge_fixture() -> Generator[None, Any, None]:
|
def mock_api():
|
||||||
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge."""
|
"""Fixture for mocking aioswitcher.api.SwitcherApi."""
|
||||||
queue = Queue()
|
api_mock = AsyncMock()
|
||||||
|
|
||||||
async def mock_queue():
|
|
||||||
"""Mock asyncio's Queue."""
|
|
||||||
await queue.put(MockSwitcherV2Device())
|
|
||||||
return await queue.get()
|
|
||||||
|
|
||||||
mock_bridge = AsyncMock()
|
|
||||||
|
|
||||||
patchers = [
|
patchers = [
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.start",
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.connect",
|
||||||
new=mock_bridge,
|
new=api_mock,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.stop",
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.disconnect",
|
||||||
new=mock_bridge,
|
new=api_mock,
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.queue",
|
|
||||||
get=mock_queue,
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.running",
|
|
||||||
return_value=True,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
for patcher in patchers:
|
for patcher in patchers:
|
||||||
patcher.start()
|
patcher.start()
|
||||||
|
|
||||||
yield
|
yield api_mock
|
||||||
|
|
||||||
for patcher in patchers:
|
|
||||||
patcher.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@fixture(name="mock_failed_bridge")
|
|
||||||
def mock_failed_bridge_fixture() -> Generator[None, Any, None]:
|
|
||||||
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge."""
|
|
||||||
|
|
||||||
async def mock_queue():
|
|
||||||
"""Mock asyncio's Queue."""
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
patchers = [
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.start",
|
|
||||||
return_value=None,
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.stop",
|
|
||||||
return_value=None,
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.queue",
|
|
||||||
get=mock_queue,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
for patcher in patchers:
|
|
||||||
patcher.start()
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
for patcher in patchers:
|
|
||||||
patcher.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@fixture(name="mock_api")
|
|
||||||
def mock_api_fixture() -> Generator[AsyncMock, Any, None]:
|
|
||||||
"""Fixture for mocking aioswitcher.api.SwitcherV2Api."""
|
|
||||||
mock_api = AsyncMock()
|
|
||||||
|
|
||||||
patchers = [
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.connect",
|
|
||||||
new=mock_api,
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.disconnect",
|
|
||||||
new=mock_api,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
for patcher in patchers:
|
|
||||||
patcher.start()
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
for patcher in patchers:
|
for patcher in patchers:
|
||||||
patcher.stop()
|
patcher.stop()
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
"""Constants for the Switcher integration tests."""
|
"""Constants for the Switcher integration tests."""
|
||||||
|
|
||||||
|
from aioswitcher.device import (
|
||||||
|
DeviceState,
|
||||||
|
DeviceType,
|
||||||
|
SwitcherPowerPlug,
|
||||||
|
SwitcherWaterHeater,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.switcher_kis import (
|
from homeassistant.components.switcher_kis import (
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DEVICE_PASSWORD,
|
CONF_DEVICE_PASSWORD,
|
||||||
@ -8,27 +15,54 @@ from homeassistant.components.switcher_kis import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DUMMY_AUTO_OFF_SET = "01:30:00"
|
DUMMY_AUTO_OFF_SET = "01:30:00"
|
||||||
DUMMY_TIMER_MINUTES_SET = "90"
|
DUMMY_AUTO_SHUT_DOWN = "02:00:00"
|
||||||
DUMMY_DEVICE_ID = "a123bc"
|
DUMMY_DEVICE_ID1 = "a123bc"
|
||||||
DUMMY_DEVICE_NAME = "Device Name"
|
DUMMY_DEVICE_ID2 = "cafe12"
|
||||||
|
DUMMY_DEVICE_NAME1 = "Plug 23BC"
|
||||||
|
DUMMY_DEVICE_NAME2 = "Heater FE12"
|
||||||
DUMMY_DEVICE_PASSWORD = "12345678"
|
DUMMY_DEVICE_PASSWORD = "12345678"
|
||||||
DUMMY_DEVICE_STATE = "on"
|
DUMMY_ELECTRIC_CURRENT1 = 0.5
|
||||||
DUMMY_ELECTRIC_CURRENT = 12.8
|
DUMMY_ELECTRIC_CURRENT2 = 12.8
|
||||||
DUMMY_ICON = "mdi:dummy-icon"
|
DUMMY_IP_ADDRESS1 = "192.168.100.157"
|
||||||
DUMMY_IP_ADDRESS = "192.168.100.157"
|
DUMMY_IP_ADDRESS2 = "192.168.100.158"
|
||||||
DUMMY_MAC_ADDRESS = "A1:B2:C3:45:67:D8"
|
DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8"
|
||||||
DUMMY_NAME = "boiler"
|
DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9"
|
||||||
DUMMY_PHONE_ID = "1234"
|
DUMMY_PHONE_ID = "1234"
|
||||||
DUMMY_POWER_CONSUMPTION = 2780
|
DUMMY_POWER_CONSUMPTION1 = 100
|
||||||
|
DUMMY_POWER_CONSUMPTION2 = 2780
|
||||||
DUMMY_REMAINING_TIME = "01:29:32"
|
DUMMY_REMAINING_TIME = "01:29:32"
|
||||||
|
DUMMY_TIMER_MINUTES_SET = "90"
|
||||||
|
|
||||||
# Adjust if any modification were made to DUMMY_DEVICE_NAME
|
YAML_CONFIG = {
|
||||||
SWITCH_ENTITY_ID = "switch.device_name"
|
|
||||||
|
|
||||||
MANDATORY_CONFIGURATION = {
|
|
||||||
DOMAIN: {
|
DOMAIN: {
|
||||||
CONF_PHONE_ID: DUMMY_PHONE_ID,
|
CONF_PHONE_ID: DUMMY_PHONE_ID,
|
||||||
CONF_DEVICE_ID: DUMMY_DEVICE_ID,
|
CONF_DEVICE_ID: DUMMY_DEVICE_ID1,
|
||||||
CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD,
|
CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DUMMY_PLUG_DEVICE = SwitcherPowerPlug(
|
||||||
|
DeviceType.POWER_PLUG,
|
||||||
|
DeviceState.ON,
|
||||||
|
DUMMY_DEVICE_ID1,
|
||||||
|
DUMMY_IP_ADDRESS1,
|
||||||
|
DUMMY_MAC_ADDRESS1,
|
||||||
|
DUMMY_DEVICE_NAME1,
|
||||||
|
DUMMY_POWER_CONSUMPTION1,
|
||||||
|
DUMMY_ELECTRIC_CURRENT1,
|
||||||
|
)
|
||||||
|
|
||||||
|
DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater(
|
||||||
|
DeviceType.V4,
|
||||||
|
DeviceState.ON,
|
||||||
|
DUMMY_DEVICE_ID2,
|
||||||
|
DUMMY_IP_ADDRESS2,
|
||||||
|
DUMMY_MAC_ADDRESS2,
|
||||||
|
DUMMY_DEVICE_NAME2,
|
||||||
|
DUMMY_POWER_CONSUMPTION2,
|
||||||
|
DUMMY_ELECTRIC_CURRENT2,
|
||||||
|
DUMMY_REMAINING_TIME,
|
||||||
|
DUMMY_AUTO_SHUT_DOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
DUMMY_SWITCHER_DEVICES = [DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE]
|
||||||
|
106
tests/components/switcher_kis/test_config_flow.py
Normal file
106
tests/components/switcher_kis/test_config_flow.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""Test the Switcher config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass):
|
||||||
|
"""Test import step."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "Switcher"
|
||||||
|
assert result["data"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mock_bridge",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
DUMMY_PLUG_DEVICE,
|
||||||
|
DUMMY_WATER_HEATER_DEVICE,
|
||||||
|
# Make sure we don't detect the same device twice
|
||||||
|
DUMMY_WATER_HEATER_DEVICE,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_user_setup(hass, mock_bridge):
|
||||||
|
"""Test we can finish a config flow."""
|
||||||
|
with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_bridge.is_running is False
|
||||||
|
assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result2["title"] == "Switcher"
|
||||||
|
assert result2["result"].data == {}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_setup_abort_no_devices_found(hass, mock_bridge):
|
||||||
|
"""Test we abort a config flow if no devices found."""
|
||||||
|
with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_bridge.is_running is False
|
||||||
|
assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result2["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result2["reason"] == "no_devices_found"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"source",
|
||||||
|
[
|
||||||
|
config_entries.SOURCE_IMPORT,
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_single_instance(hass, source):
|
||||||
|
"""Test we only allow a single config flow."""
|
||||||
|
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": source}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "single_instance_allowed"
|
@ -1,200 +1,111 @@
|
|||||||
"""Test cases for the switcher_kis component."""
|
"""Test cases for the switcher_kis component."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Generator
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from aioswitcher.consts import COMMAND_ON
|
import pytest
|
||||||
from aioswitcher.devices import SwitcherV2Device
|
|
||||||
from pytest import raises
|
|
||||||
|
|
||||||
from homeassistant.components.switcher_kis import (
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.switcher_kis.const import (
|
||||||
DATA_DEVICE,
|
DATA_DEVICE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SIGNAL_SWITCHER_DEVICE_UPDATE,
|
MAX_UPDATE_INTERVAL_SEC,
|
||||||
)
|
)
|
||||||
from homeassistant.components.switcher_kis.switch import (
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
CONF_AUTO_OFF,
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
CONF_TIMER_MINUTES,
|
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
|
||||||
)
|
|
||||||
from homeassistant.const import CONF_ENTITY_ID
|
|
||||||
from homeassistant.core import Context, HomeAssistant, callback
|
|
||||||
from homeassistant.exceptions import UnknownUser
|
|
||||||
from homeassistant.helpers.config_validation import time_period_str
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt, slugify
|
||||||
|
|
||||||
from .consts import (
|
from . import init_integration
|
||||||
DUMMY_AUTO_OFF_SET,
|
from .consts import DUMMY_SWITCHER_DEVICES, YAML_CONFIG
|
||||||
DUMMY_DEVICE_ID,
|
|
||||||
DUMMY_DEVICE_NAME,
|
|
||||||
DUMMY_DEVICE_STATE,
|
|
||||||
DUMMY_ELECTRIC_CURRENT,
|
|
||||||
DUMMY_IP_ADDRESS,
|
|
||||||
DUMMY_MAC_ADDRESS,
|
|
||||||
DUMMY_PHONE_ID,
|
|
||||||
DUMMY_POWER_CONSUMPTION,
|
|
||||||
DUMMY_REMAINING_TIME,
|
|
||||||
DUMMY_TIMER_MINUTES_SET,
|
|
||||||
MANDATORY_CONFIGURATION,
|
|
||||||
SWITCH_ENTITY_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
from tests.common import MockUser, async_fire_time_changed
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
async def test_failed_config(
|
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
|
||||||
hass: HomeAssistant, mock_failed_bridge: Generator[None, Any, None]
|
async def test_async_setup_yaml_config(hass, mock_bridge) -> None:
|
||||||
) -> None:
|
"""Test setup started by configuration from YAML."""
|
||||||
"""Test failed configuration."""
|
assert await async_setup_component(hass, DOMAIN, YAML_CONFIG)
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) is False
|
|
||||||
|
|
||||||
|
|
||||||
async def test_minimal_config(
|
|
||||||
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
|
|
||||||
) -> None:
|
|
||||||
"""Test setup with configuration minimal entries."""
|
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_data_bucket(
|
|
||||||
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
|
|
||||||
) -> None:
|
|
||||||
"""Test the event send with the updated device."""
|
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
device = hass.data[DOMAIN].get(DATA_DEVICE)
|
assert mock_bridge.is_running is True
|
||||||
assert device.device_id == DUMMY_DEVICE_ID
|
assert len(hass.data[DOMAIN]) == 2
|
||||||
assert device.ip_addr == DUMMY_IP_ADDRESS
|
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
|
||||||
assert device.mac_addr == DUMMY_MAC_ADDRESS
|
|
||||||
assert device.name == DUMMY_DEVICE_NAME
|
|
||||||
assert device.state == DUMMY_DEVICE_STATE
|
|
||||||
assert device.remaining_time == DUMMY_REMAINING_TIME
|
|
||||||
assert device.auto_off_set == DUMMY_AUTO_OFF_SET
|
|
||||||
assert device.power_consumption == DUMMY_POWER_CONSUMPTION
|
|
||||||
assert device.electric_current == DUMMY_ELECTRIC_CURRENT
|
|
||||||
assert device.phone_id == DUMMY_PHONE_ID
|
|
||||||
|
|
||||||
|
|
||||||
async def test_set_auto_off_service(
|
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
|
||||||
hass: HomeAssistant,
|
async def test_async_setup_user_config_flow(hass, mock_bridge) -> None:
|
||||||
mock_bridge: Generator[None, Any, None],
|
"""Test setup started by user config flow."""
|
||||||
mock_api: Generator[None, Any, None],
|
with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
|
||||||
hass_owner_user: MockUser,
|
result = await hass.config_entries.flow.async_init(
|
||||||
) -> None:
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
"""Test the set_auto_off service."""
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
|
await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.services.has_service(DOMAIN, SERVICE_SET_AUTO_OFF_NAME)
|
assert mock_bridge.is_running is True
|
||||||
|
assert len(hass.data[DOMAIN]) == 2
|
||||||
|
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
DOMAIN,
|
async def test_update_fail(hass, mock_bridge, caplog):
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
"""Test entities state unavailable when updates fail.."""
|
||||||
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
await init_integration(hass)
|
||||||
blocking=True,
|
assert mock_bridge
|
||||||
context=Context(user_id=hass_owner_user.id),
|
|
||||||
|
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_bridge.is_running is True
|
||||||
|
assert len(hass.data[DOMAIN]) == 2
|
||||||
|
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
|
||||||
|
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC + 1)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for device in DUMMY_SWITCHER_DEVICES:
|
||||||
|
assert (
|
||||||
|
f"Device {device.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
entity_id = f"switch.{slugify(device.name)}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
entity_id = f"sensor.{slugify(device.name)}_power_consumption"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
with raises(UnknownUser) as unknown_user_exc:
|
for device in DUMMY_SWITCHER_DEVICES:
|
||||||
await hass.services.async_call(
|
entity_id = f"switch.{slugify(device.name)}"
|
||||||
DOMAIN,
|
state = hass.states.get(entity_id)
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
assert state.state != STATE_UNAVAILABLE
|
||||||
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
|
||||||
blocking=True,
|
|
||||||
context=Context(user_id="not_real_user"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert unknown_user_exc.type is UnknownUser
|
entity_id = f"sensor.{slugify(device.name)}_power_consumption"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
with patch(
|
assert state.state != STATE_UNAVAILABLE
|
||||||
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown"
|
|
||||||
) as mock_set_auto_shutdown:
|
|
||||||
await hass.services.async_call(
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_SET_AUTO_OFF_NAME,
|
|
||||||
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
mock_set_auto_shutdown.assert_called_once_with(
|
|
||||||
time_period_str(DUMMY_AUTO_OFF_SET)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on_with_timer_service(
|
async def test_entry_unload(hass, mock_bridge):
|
||||||
hass: HomeAssistant,
|
"""Test entry unload."""
|
||||||
mock_bridge: Generator[None, Any, None],
|
entry = await init_integration(hass)
|
||||||
mock_api: Generator[None, Any, None],
|
assert mock_bridge
|
||||||
hass_owner_user: MockUser,
|
|
||||||
) -> None:
|
|
||||||
"""Test the set_auto_off service."""
|
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
|
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
assert mock_bridge.is_running is True
|
||||||
|
assert len(hass.data[DOMAIN]) == 2
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME)
|
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert mock_bridge.is_running is False
|
||||||
await hass.services.async_call(
|
assert len(hass.data[DOMAIN]) == 0
|
||||||
DOMAIN,
|
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
|
||||||
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET},
|
|
||||||
blocking=True,
|
|
||||||
context=Context(user_id=hass_owner_user.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
with raises(UnknownUser) as unknown_user_exc:
|
|
||||||
await hass.services.async_call(
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
|
||||||
{
|
|
||||||
CONF_ENTITY_ID: SWITCH_ENTITY_ID,
|
|
||||||
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
context=Context(user_id="not_real_user"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert unknown_user_exc.type is UnknownUser
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device"
|
|
||||||
) as mock_control_device:
|
|
||||||
await hass.services.async_call(
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
|
||||||
{
|
|
||||||
CONF_ENTITY_ID: SWITCH_ENTITY_ID,
|
|
||||||
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
mock_control_device.assert_called_once_with(
|
|
||||||
COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_signal_dispatcher(
|
|
||||||
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
|
|
||||||
) -> None:
|
|
||||||
"""Test signal dispatcher dispatching device updates every 4 seconds."""
|
|
||||||
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def verify_update_data(device: SwitcherV2Device) -> None:
|
|
||||||
"""Use as callback for signal dispatcher."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async_dispatcher_connect(hass, SIGNAL_SWITCHER_DEVICE_UPDATE, verify_update_data)
|
|
||||||
|
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=5))
|
|
||||||
|
93
tests/components/switcher_kis/test_sensor.py
Normal file
93
tests/components/switcher_kis/test_sensor.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"""Test the Switcher Sensor Platform."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.switcher_kis.const import DATA_DEVICE, DOMAIN
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import init_integration
|
||||||
|
from .consts import DUMMY_PLUG_DEVICE, DUMMY_SWITCHER_DEVICES, DUMMY_WATER_HEATER_DEVICE
|
||||||
|
|
||||||
|
DEVICE_SENSORS_TUPLE = (
|
||||||
|
(
|
||||||
|
DUMMY_PLUG_DEVICE,
|
||||||
|
[
|
||||||
|
"power_consumption",
|
||||||
|
"electric_current",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DUMMY_WATER_HEATER_DEVICE,
|
||||||
|
[
|
||||||
|
"power_consumption",
|
||||||
|
"electric_current",
|
||||||
|
"remaining_time",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
|
||||||
|
async def test_sensor_platform(hass, mock_bridge):
|
||||||
|
"""Test sensor platform."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
assert mock_bridge.is_running is True
|
||||||
|
assert len(hass.data[DOMAIN]) == 2
|
||||||
|
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
|
||||||
|
|
||||||
|
for device, sensors in DEVICE_SENSORS_TUPLE:
|
||||||
|
for sensor in sensors:
|
||||||
|
entity_id = f"sensor.{slugify(device.name)}_{sensor}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == str(getattr(device, sensor))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_disabled(hass, mock_bridge):
|
||||||
|
"""Test sensor disabled by default."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
unique_id = f"{device.device_id}-{device.mac_address}-auto_off_set"
|
||||||
|
entity_id = f"sensor.{slugify(device.name)}_auto_shutdown"
|
||||||
|
entry = registry.async_get(entity_id)
|
||||||
|
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == unique_id
|
||||||
|
assert entry.disabled is True
|
||||||
|
assert entry.disabled_by == er.DISABLED_INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = registry.async_update_entity(
|
||||||
|
entry.entity_id, **{"disabled_by": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
|
||||||
|
async def test_sensor_update(hass, mock_bridge, monkeypatch):
|
||||||
|
"""Test sensor update."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
sensor = "power_consumption"
|
||||||
|
entity_id = f"sensor.{slugify(device.name)}_{sensor}"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == str(getattr(device, sensor))
|
||||||
|
|
||||||
|
monkeypatch.setattr(device, sensor, 1431)
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "1431"
|
162
tests/components/switcher_kis/test_services.py
Normal file
162
tests/components/switcher_kis/test_services.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
"""Test the services for the Switcher integration."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aioswitcher.api import Command
|
||||||
|
from aioswitcher.device import DeviceState
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.components.switcher_kis.const import (
|
||||||
|
CONF_AUTO_OFF,
|
||||||
|
CONF_TIMER_MINUTES,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||||
|
from homeassistant.helpers.config_validation import time_period_str
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import init_integration
|
||||||
|
from .consts import (
|
||||||
|
DUMMY_AUTO_OFF_SET,
|
||||||
|
DUMMY_PLUG_DEVICE,
|
||||||
|
DUMMY_TIMER_MINUTES_SET,
|
||||||
|
DUMMY_WATER_HEATER_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
|
||||||
|
async def test_turn_on_with_timer_service(hass, mock_bridge, mock_api, monkeypatch):
|
||||||
|
"""Test the turn on with timer service."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
# Test initial state - off
|
||||||
|
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device"
|
||||||
|
) as mock_control_device:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 2
|
||||||
|
mock_control_device.assert_called_once_with(
|
||||||
|
Command.ON, int(DUMMY_TIMER_MINUTES_SET)
|
||||||
|
)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
|
||||||
|
async def test_set_auto_off_service(hass, mock_bridge, mock_api):
|
||||||
|
"""Test the set auto off service."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown"
|
||||||
|
) as mock_set_auto_shutdown:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 2
|
||||||
|
mock_set_auto_shutdown.assert_called_once_with(
|
||||||
|
time_period_str(DUMMY_AUTO_OFF_SET)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
|
||||||
|
async def test_set_auto_off_service_fail(hass, mock_bridge, mock_api, caplog):
|
||||||
|
"""Test set auto off service failed."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown",
|
||||||
|
return_value=None,
|
||||||
|
) as mock_set_auto_shutdown:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 2
|
||||||
|
mock_set_auto_shutdown.assert_called_once_with(
|
||||||
|
time_period_str(DUMMY_AUTO_OFF_SET)
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
f"Call api for {device.name} failed, api: 'set_auto_shutdown'"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True)
|
||||||
|
async def test_plug_unsupported_services(hass, mock_bridge, mock_api, caplog):
|
||||||
|
"""Test plug device unsupported services."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_PLUG_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
# Turn on with timer
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 0
|
||||||
|
assert (
|
||||||
|
f"Service '{SERVICE_TURN_ON_WITH_TIMER_NAME}' is not supported by {device.name}"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auto off
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_AUTO_OFF_NAME,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 0
|
||||||
|
assert (
|
||||||
|
f"Service '{SERVICE_SET_AUTO_OFF_NAME}' is not supported by {device.name}"
|
||||||
|
in caplog.text
|
||||||
|
)
|
127
tests/components/switcher_kis/test_switch.py
Normal file
127
tests/components/switcher_kis/test_switch.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Test the Switcher switch platform."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aioswitcher.api import Command, SwitcherBaseResponse
|
||||||
|
from aioswitcher.device import DeviceState
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import init_integration
|
||||||
|
from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
|
||||||
|
async def test_switch(hass, mock_bridge, mock_api, monkeypatch):
|
||||||
|
"""Test the switch."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_WATER_HEATER_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
# Test initial state - on
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
# Test state change on --> off
|
||||||
|
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
# Test turning on
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
|
||||||
|
) as mock_control_device:
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 2
|
||||||
|
mock_control_device.assert_called_once_with(Command.ON)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
# Test turning off
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device"
|
||||||
|
) as mock_control_device:
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 4
|
||||||
|
mock_control_device.assert_called_once_with(Command.OFF)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True)
|
||||||
|
async def test_switch_control_fail(hass, mock_bridge, mock_api, monkeypatch, caplog):
|
||||||
|
"""Test switch control fail."""
|
||||||
|
await init_integration(hass)
|
||||||
|
assert mock_bridge
|
||||||
|
|
||||||
|
device = DUMMY_PLUG_DEVICE
|
||||||
|
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
|
||||||
|
|
||||||
|
# Test initial state - off
|
||||||
|
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
# Test exception during turn on
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
|
||||||
|
side_effect=RuntimeError("fake error"),
|
||||||
|
) as mock_control_device:
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 2
|
||||||
|
mock_control_device.assert_called_once_with(Command.ON)
|
||||||
|
assert (
|
||||||
|
f"Call api for {device.name} failed, api: 'control_device'" in caplog.text
|
||||||
|
)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
# Make device available again
|
||||||
|
mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
# Test error response during turn on
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
|
||||||
|
return_value=SwitcherBaseResponse(None),
|
||||||
|
) as mock_control_device:
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_api.call_count == 4
|
||||||
|
mock_control_device.assert_called_once_with(Command.ON)
|
||||||
|
assert (
|
||||||
|
f"Call api for {device.name} failed, api: 'control_device'" in caplog.text
|
||||||
|
)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user