Add mypy check, add missing types and fix type issues

This commit is contained in:
Andre Basche 2023-07-23 21:52:42 +02:00
parent f0fb5742a4
commit 9d6b8297b2
19 changed files with 542 additions and 239 deletions

View File

@ -24,12 +24,17 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install flake8 pylint black python -m pip install -r requirements.txt
python -m pip install -r requirements_dev.txt
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Type check with mypy
run: |
touch "$(python -c 'import inspect, homeassistant, os; print(os.path.dirname(inspect.getfile(homeassistant)))')"/py.typed
mypy -p custom_components.hon
# - name: Analysing the code with pylint # - name: Analysing the code with pylint
# run: | # run: |
# pylint --max-line-length 88 $(git ls-files '*.py') # pylint --max-line-length 88 $(git ls-files '*.py')

View File

@ -1,7 +1,7 @@
import logging import logging
from pathlib import Path from pathlib import Path
import voluptuous as vol import voluptuous as vol # type: ignore[import]
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers import config_validation as cv, aiohttp_client from homeassistant.helpers import config_validation as cv, aiohttp_client
@ -25,13 +25,15 @@ CONFIG_SCHEMA = vol.Schema(
) )
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None:
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
if (config_dir := hass.config.config_dir) is None:
raise ValueError("Missing Config Dir")
hon = await Hon( hon = await Hon(
entry.data["email"], entry.data["email"],
entry.data["password"], entry.data["password"],
session=session, session=session,
test_data_path=Path(hass.config.config_dir), test_data_path=Path(config_dir),
).create() ).create()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = hon hass.data[DOMAIN][entry.unique_id] = hon
@ -41,10 +43,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)
) )
return True return
async def async_unload_entry(hass, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload: if unload:
if not hass.data[DOMAIN]: if not hass.data[DOMAIN]:

View File

@ -8,6 +8,8 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN from .const import DOMAIN
from .hon import HonEntity, unique_entities from .hon import HonEntity, unique_entities
@ -287,7 +289,9 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
BINARY_SENSORS["WD"] = unique_entities(BINARY_SENSORS["WM"], BINARY_SENSORS["TD"]) BINARY_SENSORS["WD"] = unique_entities(BINARY_SENSORS["WM"], BINARY_SENSORS["TD"])
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in BINARY_SENSORS.get(device.appliance_type, []): for description in BINARY_SENSORS.get(device.appliance_type, []):
@ -304,13 +308,13 @@ class HonBinarySensorEntity(HonEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
return ( return bool(
self._device.get(self.entity_description.key, "") self._device.get(self.entity_description.key, "")
== self.entity_description.on_value == self.entity_description.on_value
) )
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_native_value = ( self._attr_native_value = (
self._device.get(self.entity_description.key, "") self._device.get(self.entity_description.key, "")
== self.entity_description.on_value == self.entity_description.on_value

View File

@ -5,10 +5,13 @@ from homeassistant.components import persistent_notification
from homeassistant.components.button import ButtonEntityDescription, ButtonEntity from homeassistant.components.button import ButtonEntityDescription, ButtonEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from .const import DOMAIN from .const import DOMAIN
from .hon import HonEntity from .hon import HonEntity
from .typedefs import HonButtonType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,8 +41,10 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = {
} }
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
entities = [] hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities: list[HonButtonType] = []
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in BUTTONS.get(device.appliance_type, []): for description in BUTTONS.get(device.appliance_type, []):
if not device.commands.get(description.key): if not device.commands.get(description.key):
@ -70,7 +75,9 @@ class HonButtonEntity(HonEntity, ButtonEntity):
class HonDeviceInfo(HonEntity, ButtonEntity): class HonDeviceInfo(HonEntity, ButtonEntity):
def __init__(self, hass, entry, device: HonAppliance) -> None: def __init__(
self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance
) -> None:
super().__init__(hass, entry, device) super().__init__(hass, entry, device)
self._attr_unique_id = f"{super().unique_id}_show_device_info" self._attr_unique_id = f"{super().unique_id}_show_device_info"
@ -93,7 +100,9 @@ class HonDeviceInfo(HonEntity, ButtonEntity):
class HonDataArchive(HonEntity, ButtonEntity): class HonDataArchive(HonEntity, ButtonEntity):
def __init__(self, hass, entry, device: HonAppliance) -> None: def __init__(
self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance
) -> None:
super().__init__(hass, entry, device) super().__init__(hass, entry, device)
self._attr_unique_id = f"{super().unique_id}_create_data_archive" self._attr_unique_id = f"{super().unique_id}_create_data_archive"
@ -104,7 +113,9 @@ class HonDataArchive(HonEntity, ButtonEntity):
self._attr_entity_registry_enabled_default = False self._attr_entity_registry_enabled_default = False
async def async_press(self) -> None: async def async_press(self) -> None:
path = Path(self._hass.config.config_dir) / "www" if (config_dir := self._hass.config.config_dir) is None:
raise ValueError("Missing Config Dir")
path = Path(config_dir) / "www"
data = await self._device.data_archive(path) data = await self._device.data_archive(path)
title = f"{self._device.nick_name} Data Archive" title = f"{self._device.nick_name} Data Archive"
text = ( text = (

View File

@ -1,5 +1,6 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
@ -19,7 +20,10 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from pyhon.parameter.range import HonParameterRange
from .const import HON_HVAC_MODE, HON_FAN, DOMAIN, HON_HVAC_PROGRAM from .const import HON_HVAC_MODE, HON_FAN, DOMAIN, HON_HVAC_PROGRAM
from .hon import HonEntity from .hon import HonEntity
@ -34,10 +38,12 @@ class HonACClimateEntityDescription(ClimateEntityDescription):
@dataclass @dataclass
class HonClimateEntityDescription(ClimateEntityDescription): class HonClimateEntityDescription(ClimateEntityDescription):
mode: HVACMode = "auto" mode: HVACMode = HVACMode.AUTO
CLIMATES = { CLIMATES: dict[
str, tuple[HonACClimateEntityDescription | HonClimateEntityDescription, ...]
] = {
"AC": ( "AC": (
HonACClimateEntityDescription( HonACClimateEntityDescription(
key="settings", key="settings",
@ -90,8 +96,11 @@ CLIMATES = {
} }
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
entity: HonClimateEntity | HonACClimateEntity
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in CLIMATES.get(device.appliance_type, []): for description in CLIMATES.get(device.appliance_type, []):
if isinstance(description, HonACClimateEntityDescription): if isinstance(description, HonACClimateEntityDescription):
@ -103,14 +112,22 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
continue continue
entity = HonClimateEntity(hass, entry, device, description) entity = HonClimateEntity(hass, entry, device, description)
else: else:
continue continue # type: ignore[unreachable]
await entity.coordinator.async_config_entry_first_refresh() await entity.coordinator.async_config_entry_first_refresh()
entities.append(entity) entities.append(entity)
async_add_entities(entities) async_add_entities(entities)
class HonACClimateEntity(HonEntity, ClimateEntity): class HonACClimateEntity(HonEntity, ClimateEntity):
def __init__(self, hass, entry, device: HonAppliance, description) -> None: entity_description: HonACClimateEntityDescription
def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: HonACClimateEntityDescription,
) -> None:
super().__init__(hass, entry, device, description) super().__init__(hass, entry, device, description)
self._attr_temperature_unit = TEMP_CELSIUS self._attr_temperature_unit = TEMP_CELSIUS
@ -138,37 +155,38 @@ class HonACClimateEntity(HonEntity, ClimateEntity):
self._handle_coordinator_update(update=False) self._handle_coordinator_update(update=False)
def _set_temperature_bound(self) -> None: def _set_temperature_bound(self) -> None:
self._attr_target_temperature_step = self._device.settings[ temperature = self._device.settings[self.entity_description.key]
"settings.tempSel" if not isinstance(temperature, HonParameterRange):
].step raise ValueError
self._attr_max_temp = self._device.settings["settings.tempSel"].max self._attr_max_temp = temperature.max
self._attr_min_temp = self._device.settings["settings.tempSel"].min self._attr_target_temperature_step = temperature.step
self._attr_min_temp = temperature.min
@property @property
def target_temperature(self) -> int | None: def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._device.get("tempSel") return self._device.get("tempSel", 0.0)
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
return self._device.get("tempIndoor") return self._device.get("tempIndoor", 0.0)
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs: Any) -> None:
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return False return
self._device.settings["settings.tempSel"].value = str(int(temperature)) self._device.settings["settings.tempSel"].value = str(int(temperature))
await self._device.commands["settings"].send() await self._device.commands["settings"].send()
self.async_write_ha_state() self.async_write_ha_state()
@property @property
def hvac_mode(self) -> HVACMode | str | None: def hvac_mode(self) -> HVACMode:
if self._device.get("onOffStatus") == 0: if self._device.get("onOffStatus") == 0:
return HVACMode.OFF return HVACMode.OFF
else: else:
return HON_HVAC_MODE[self._device.get("machMode")] return HON_HVAC_MODE[self._device.get("machMode")]
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
self._attr_hvac_mode = hvac_mode self._attr_hvac_mode = hvac_mode
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
await self._device.commands["stopProgram"].send() await self._device.commands["stopProgram"].send()
@ -215,7 +233,7 @@ class HonACClimateEntity(HonEntity, ClimateEntity):
"""Return the fan setting.""" """Return the fan setting."""
return HON_FAN[self._device.get("windSpeed")] return HON_FAN[self._device.get("windSpeed")]
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode: str) -> None:
fan_modes = {} fan_modes = {}
for mode in reversed(self._device.settings["settings.windSpeed"].values): for mode in reversed(self._device.settings["settings.windSpeed"].values):
fan_modes[HON_FAN[int(mode)]] = mode fan_modes[HON_FAN[int(mode)]] = mode
@ -231,14 +249,13 @@ class HonACClimateEntity(HonEntity, ClimateEntity):
vertical = self._device.get("windDirectionVertical") vertical = self._device.get("windDirectionVertical")
if horizontal == 7 and vertical == 8: if horizontal == 7 and vertical == 8:
return SWING_BOTH return SWING_BOTH
elif horizontal == 7: if horizontal == 7:
return SWING_HORIZONTAL return SWING_HORIZONTAL
elif vertical == 8: if vertical == 8:
return SWING_VERTICAL return SWING_VERTICAL
else: return SWING_OFF
return SWING_OFF
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode: str) -> None:
horizontal = self._device.settings["settings.windDirectionHorizontal"] horizontal = self._device.settings["settings.windDirectionHorizontal"]
vertical = self._device.settings["settings.windDirectionVertical"] vertical = self._device.settings["settings.windDirectionVertical"]
if swing_mode in [SWING_BOTH, SWING_HORIZONTAL]: if swing_mode in [SWING_BOTH, SWING_HORIZONTAL]:
@ -254,13 +271,7 @@ class HonACClimateEntity(HonEntity, ClimateEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_target_temperature = self.target_temperature
self._attr_current_temperature = self.current_temperature
self._attr_hvac_mode = self.hvac_mode
self._attr_fan_modes = self.fan_modes
self._attr_fan_mode = self.fan_mode
self._attr_swing_mode = self.swing_mode
if update: if update:
self.async_write_ha_state() self.async_write_ha_state()
@ -268,7 +279,13 @@ class HonACClimateEntity(HonEntity, ClimateEntity):
class HonClimateEntity(HonEntity, ClimateEntity): class HonClimateEntity(HonEntity, ClimateEntity):
entity_description: HonClimateEntityDescription entity_description: HonClimateEntityDescription
def __init__(self, hass, entry, device: HonAppliance, description) -> None: def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: HonClimateEntityDescription,
) -> None:
super().__init__(hass, entry, device, description) super().__init__(hass, entry, device, description)
self._attr_temperature_unit = TEMP_CELSIUS self._attr_temperature_unit = TEMP_CELSIUS
@ -288,7 +305,9 @@ class HonClimateEntity(HonEntity, ClimateEntity):
for mode, data in device.commands["startProgram"].categories.items(): for mode, data in device.commands["startProgram"].categories.items():
if mode not in data.parameters["program"].values: if mode not in data.parameters["program"].values:
continue continue
if zone := data.parameters.get("zone"): if (zone := data.parameters.get("zone")) and isinstance(
self.entity_description.name, str
):
if self.entity_description.name.lower() in zone.values: if self.entity_description.name.lower() in zone.values:
modes.append(mode) modes.append(mode)
else: else:
@ -300,29 +319,29 @@ class HonClimateEntity(HonEntity, ClimateEntity):
@property @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._device.get(self.entity_description.key) return self._device.get(self.entity_description.key, 0.0)
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
temp_key = self.entity_description.key.split(".")[-1].replace("Sel", "") temp_key = self.entity_description.key.split(".")[-1].replace("Sel", "")
return self._device.get(temp_key) return self._device.get(temp_key, 0.0)
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs: Any) -> None:
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return False return
self._device.settings[self.entity_description.key].value = str(int(temperature)) self._device.settings[self.entity_description.key].value = str(int(temperature))
await self._device.commands["settings"].send() await self._device.commands["settings"].send()
self.async_write_ha_state() self.async_write_ha_state()
@property @property
def hvac_mode(self) -> HVACMode | str | None: def hvac_mode(self) -> HVACMode:
if self._device.get("onOffStatus") == 0: if self._device.get("onOffStatus") == 0:
return HVACMode.OFF return HVACMode.OFF
else: else:
return self.entity_description.mode return self.entity_description.mode
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if len(self.hvac_modes) <= 1: if len(self.hvac_modes) <= 1:
return return
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
@ -347,7 +366,8 @@ class HonClimateEntity(HonEntity, ClimateEntity):
command = "stopProgram" if preset_mode == "no_mode" else "startProgram" command = "stopProgram" if preset_mode == "no_mode" else "startProgram"
if program := self._device.settings.get(f"{command}.program"): if program := self._device.settings.get(f"{command}.program"):
program.value = preset_mode program.value = preset_mode
if zone := self._device.settings.get(f"{command}.zone"): zone = self._device.settings.get(f"{command}.zone")
if zone and isinstance(self.entity_description.name, str):
zone.value = self.entity_description.name.lower() zone.value = self.entity_description.name.lower()
self._device.sync_command(command, "settings") self._device.sync_command(command, "settings")
self._set_temperature_bound() self._set_temperature_bound()
@ -356,18 +376,15 @@ class HonClimateEntity(HonEntity, ClimateEntity):
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
self.async_write_ha_state() self.async_write_ha_state()
def _set_temperature_bound(self): def _set_temperature_bound(self) -> None:
self._attr_target_temperature_step = self._device.settings[ temperature = self._device.settings[self.entity_description.key]
self.entity_description.key if not isinstance(temperature, HonParameterRange):
].step raise ValueError
self._attr_max_temp = self._device.settings[self.entity_description.key].max self._attr_max_temp = temperature.max
self._attr_min_temp = self._device.settings[self.entity_description.key].min self._attr_target_temperature_step = temperature.step
self._attr_min_temp = temperature.min
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_target_temperature = self.target_temperature
self._attr_current_temperature = self.current_temperature
self._attr_hvac_mode = self.hvac_mode
self._attr_preset_mode = self.preset_mode
if update: if update:
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -1,8 +1,10 @@
import logging import logging
from typing import Any
import voluptuous as vol import voluptuous as vol # type: ignore[import]
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN from .const import DOMAIN
@ -13,11 +15,13 @@ class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self): def __init__(self) -> None:
self._email = None self._email: str | None = None
self._password = None self._password: str | None = None
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -29,6 +33,14 @@ class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._email = user_input[CONF_EMAIL] self._email = user_input[CONF_EMAIL]
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]
if self._email is None or self._password is None:
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
),
)
# Check if already configured # Check if already configured
await self.async_set_unique_id(self._email) await self.async_set_unique_id(self._email)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
@ -41,5 +53,5 @@ class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}, },
) )
async def async_step_import(self, user_input=None): async def async_step_import(self, user_input: dict[str, str]) -> FlowResult:
return await self.async_step_user(user_input) return await self.async_step_user(user_input)

View File

@ -6,10 +6,10 @@ from homeassistant.components.climate import (
FAN_AUTO, FAN_AUTO,
) )
DOMAIN = "hon" DOMAIN: str = "hon"
UPDATE_INTERVAL = 10 UPDATE_INTERVAL: int = 10
PLATFORMS = [ PLATFORMS: list[str] = [
"sensor", "sensor",
"select", "select",
"number", "number",
@ -22,7 +22,7 @@ PLATFORMS = [
"lock", "lock",
] ]
APPLIANCES = { APPLIANCES: dict[str, str] = {
"AC": "Air Conditioner", "AC": "Air Conditioner",
"AP": "Air Purifier", "AP": "Air Purifier",
"AS": "Air Scanner", "AS": "Air Scanner",
@ -40,7 +40,7 @@ APPLIANCES = {
"WM": "Washing Machine", "WM": "Washing Machine",
} }
HON_HVAC_MODE = { HON_HVAC_MODE: dict[int, HVACMode] = {
0: HVACMode.AUTO, 0: HVACMode.AUTO,
1: HVACMode.COOL, 1: HVACMode.COOL,
2: HVACMode.DRY, 2: HVACMode.DRY,
@ -50,7 +50,7 @@ HON_HVAC_MODE = {
6: HVACMode.FAN_ONLY, 6: HVACMode.FAN_ONLY,
} }
HON_HVAC_PROGRAM = { HON_HVAC_PROGRAM: dict[str, str] = {
HVACMode.AUTO: "iot_auto", HVACMode.AUTO: "iot_auto",
HVACMode.COOL: "iot_cool", HVACMode.COOL: "iot_cool",
HVACMode.DRY: "iot_dry", HVACMode.DRY: "iot_dry",
@ -58,7 +58,7 @@ HON_HVAC_PROGRAM = {
HVACMode.FAN_ONLY: "iot_fan", HVACMode.FAN_ONLY: "iot_fan",
} }
HON_FAN = { HON_FAN: dict[int, str] = {
1: FAN_HIGH, 1: FAN_HIGH,
2: FAN_MEDIUM, 2: FAN_MEDIUM,
3: FAN_LOW, 3: FAN_LOW,
@ -67,7 +67,7 @@ HON_FAN = {
} }
# These languages are official supported by hOn # These languages are official supported by hOn
LANGUAGES = [ LANGUAGES: list[str] = [
"cs", # Czech "cs", # Czech
"de", # German "de", # German
"el", # Greek "el", # Greek
@ -89,7 +89,7 @@ LANGUAGES = [
"zh", # Chinese "zh", # Chinese
] ]
WASHING_PR_PHASE = { WASHING_PR_PHASE: dict[int, str] = {
0: "ready", 0: "ready",
1: "washing", 1: "washing",
2: "washing", 2: "washing",
@ -116,7 +116,7 @@ WASHING_PR_PHASE = {
27: "washing", 27: "washing",
} }
MACH_MODE = { MACH_MODE: dict[int, str] = {
0: "ready", # NO_STATE 0: "ready", # NO_STATE
1: "ready", # SELECTION_MODE 1: "ready", # SELECTION_MODE
2: "running", # EXECUTION_MODE 2: "running", # EXECUTION_MODE
@ -129,7 +129,7 @@ MACH_MODE = {
9: "ending", # STOP_MODE 9: "ending", # STOP_MODE
} }
TUMBLE_DRYER_PR_PHASE = { TUMBLE_DRYER_PR_PHASE: dict[int, str] = {
0: "ready", 0: "ready",
1: "heat_stroke", 1: "heat_stroke",
2: "drying", 2: "drying",
@ -147,21 +147,21 @@ TUMBLE_DRYER_PR_PHASE = {
20: "drying", 20: "drying",
} }
DIRTY_LEVEL = { DIRTY_LEVEL: dict[int, str] = {
0: "unknown", 0: "unknown",
1: "little", 1: "little",
2: "normal", 2: "normal",
3: "very", 3: "very",
} }
STEAM_LEVEL = { STEAM_LEVEL: dict[int, str] = {
0: "no_steam", 0: "no_steam",
1: "cotton", 1: "cotton",
2: "delicate", 2: "delicate",
3: "synthetic", 3: "synthetic",
} }
DISHWASHER_PR_PHASE = { DISHWASHER_PR_PHASE: dict[int, str] = {
0: "ready", 0: "ready",
1: "prewash", 1: "prewash",
2: "washing", 2: "washing",
@ -171,7 +171,7 @@ DISHWASHER_PR_PHASE = {
6: "hot_rinse", 6: "hot_rinse",
} }
TUMBLE_DRYER_DRY_LEVEL = { TUMBLE_DRYER_DRY_LEVEL: dict[int, str] = {
0: "no_dry", 0: "no_dry",
1: "iron_dry", 1: "iron_dry",
2: "no_dry_iron", 2: "no_dry_iron",
@ -184,7 +184,7 @@ TUMBLE_DRYER_DRY_LEVEL = {
15: "extra_dry", 15: "extra_dry",
} }
AC_MACH_MODE = { AC_MACH_MODE: dict[int, str] = {
0: "auto", 0: "auto",
1: "cool", 1: "cool",
2: "cool", 2: "cool",
@ -194,7 +194,7 @@ AC_MACH_MODE = {
6: "fan", 6: "fan",
} }
AC_FAN_MODE = { AC_FAN_MODE: dict[int, str] = {
1: "high", 1: "high",
2: "mid", 2: "mid",
3: "low", 3: "low",
@ -202,14 +202,14 @@ AC_FAN_MODE = {
5: "auto", 5: "auto",
} }
AC_HUMAN_SENSE = { AC_HUMAN_SENSE: dict[int, str] = {
0: "touch_off", 0: "touch_off",
1: "avoid_touch", 1: "avoid_touch",
2: "follow_touch", 2: "follow_touch",
3: "unknown", 3: "unknown",
} }
AP_MACH_MODE = { AP_MACH_MODE: dict[int, str] = {
0: "standby", 0: "standby",
1: "sleep", 1: "sleep",
2: "auto", 2: "auto",
@ -217,7 +217,7 @@ AP_MACH_MODE = {
4: "max", 4: "max",
} }
AP_DIFFUSER_LEVEL = { AP_DIFFUSER_LEVEL: dict[int, str] = {
0: "off", 0: "off",
1: "soft", 1: "soft",
2: "mid", 2: "mid",
@ -225,4 +225,4 @@ AP_DIFFUSER_LEVEL = {
4: "custom", 4: "custom",
} }
REF_HUMIDITY_LEVELS = {1: "low", 2: "mid", 3: "high"} REF_HUMIDITY_LEVELS: dict[int, str] = {1: "low", 2: "mid", 3: "high"}

View File

@ -1,6 +1,5 @@
import logging import logging
import math import math
from dataclasses import dataclass
from typing import Any from typing import Any
from homeassistant.components.fan import ( from homeassistant.components.fan import (
@ -10,6 +9,8 @@ from homeassistant.components.fan import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
@ -19,18 +20,14 @@ from pyhon.parameter.range import HonParameterRange
from .const import DOMAIN from .const import DOMAIN
from .hon import HonEntity from .hon import HonEntity
from .typedefs import HonEntityDescription
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@dataclass FANS: dict[str, tuple[FanEntityDescription, ...]] = {
class HonFanEntityDescription(FanEntityDescription):
pass
FANS = {
"HO": ( "HO": (
HonFanEntityDescription( FanEntityDescription(
key="settings.windSpeed", key="settings.windSpeed",
name="Wind Speed", name="Wind Speed",
translation_key="air_extraction", translation_key="air_extraction",
@ -39,30 +36,36 @@ FANS = {
} }
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in FANS.get(device.appliance_type, []): for description in FANS.get(device.appliance_type, []):
if isinstance(description, HonFanEntityDescription): if (
if ( description.key not in device.available_settings
description.key not in device.available_settings or device.get(description.key.split(".")[-1]) is None
or device.get(description.key.split(".")[-1]) is None ):
):
continue
entity = HonFanEntity(hass, entry, device, description)
else:
continue continue
entity = HonFanEntity(hass, entry, device, description)
await entity.coordinator.async_config_entry_first_refresh() await entity.coordinator.async_config_entry_first_refresh()
entities.append(entity) entities.append(entity)
async_add_entities(entities) async_add_entities(entities)
class HonFanEntity(HonEntity, FanEntity): class HonFanEntity(HonEntity, FanEntity):
entity_description: HonFanEntityDescription entity_description: FanEntityDescription
def __init__(self, hass, entry, device: HonAppliance, description) -> None: def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: FanEntityDescription,
) -> None:
self._attr_supported_features = FanEntityFeature.SET_SPEED self._attr_supported_features = FanEntityFeature.SET_SPEED
self._wind_speed: HonParameterRange = device.settings.get(description.key) self._wind_speed: HonParameterRange
self._speed_range: tuple[int, int]
self._command, self._parameter = description.key.split(".") self._command, self._parameter = description.key.split(".")
super().__init__(hass, entry, device, description) super().__init__(hass, entry, device, description)
@ -89,8 +92,10 @@ class HonFanEntity(HonEntity, FanEntity):
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return true if device is on.""" """Return true if device is on."""
if self.percentage is None:
return False
mode = math.ceil(percentage_to_ranged_value(self._speed_range, self.percentage)) mode = math.ceil(percentage_to_ranged_value(self._speed_range, self.percentage))
return mode > self._wind_speed.min return bool(mode > self._wind_speed.min)
async def async_turn_on( async def async_turn_on(
self, self,
@ -112,9 +117,10 @@ class HonFanEntity(HonEntity, FanEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._wind_speed = self._device.settings.get(self.entity_description.key) wind_speed = self._device.settings.get(self.entity_description.key)
if len(self._wind_speed.values) > 1: if isinstance(wind_speed, HonParameterRange) and len(wind_speed.values) > 1:
self._wind_speed = wind_speed
self._speed_range = ( self._speed_range = (
int(self._wind_speed.values[1]), int(self._wind_speed.values[1]),
int(self._wind_speed.values[-1]), int(self._wind_speed.values[-1]),

View File

@ -3,23 +3,79 @@ import logging
from contextlib import suppress from contextlib import suppress
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Optional, Any, TypeVar
import pkg_resources import pkg_resources
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from .const import DOMAIN, UPDATE_INTERVAL from .const import DOMAIN, UPDATE_INTERVAL
from .typedefs import HonEntityDescription, HonOptionEntityDescription, T
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class HonEntity(CoordinatorEntity): class HonInfo:
def __init__(self) -> None:
self._manifest: dict[str, Any] = self._get_manifest()
self._hon_version: str = self._manifest.get("version", "")
self._pyhon_version: str = pkg_resources.get_distribution("pyhon").version
@staticmethod
def _get_manifest() -> dict[str, Any]:
manifest = Path(__file__).parent / "manifest.json"
with open(manifest, "r", encoding="utf-8") as file:
result: dict[str, Any] = json.loads(file.read())
return result
@property
def manifest(self) -> dict[str, Any]:
return self._manifest
@property
def hon_version(self) -> str:
return self._hon_version
@property
def pyhon_version(self) -> str:
return self._pyhon_version
class HonCoordinator(DataUpdateCoordinator[None]):
def __init__(self, hass: HomeAssistantType, device: HonAppliance):
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
name=device.unique_id,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
self._device = device
self._info = HonInfo()
async def _async_update_data(self) -> None:
return await self._device.update()
@property
def info(self) -> HonInfo:
return self._info
class HonEntity(CoordinatorEntity[HonCoordinator]):
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__(self, hass, entry, device: HonAppliance, description=None) -> None: def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: Optional[HonEntityDescription] = None,
) -> None:
coordinator = get_coordinator(hass, device) coordinator = get_coordinator(hass, device)
super().__init__(coordinator) super().__init__(coordinator)
@ -36,7 +92,7 @@ class HonEntity(CoordinatorEntity):
self._handle_coordinator_update(update=False) self._handle_coordinator_update(update=False)
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device.unique_id)}, identifiers={(DOMAIN, self._device.unique_id)},
manufacturer=self._device.get("brand", ""), manufacturer=self._device.get("brand", ""),
@ -51,71 +107,34 @@ class HonEntity(CoordinatorEntity):
self.async_write_ha_state() self.async_write_ha_state()
class HonInfo: def unique_entities(
def __init__(self): base_entities: tuple[T, ...],
self._manifest = self._get_manifest() new_entities: tuple[T, ...],
self._hon_version = self._manifest.get("version", "") ) -> tuple[T, ...]:
self._pyhon_version = pkg_resources.get_distribution("pyhon").version
@staticmethod
def _get_manifest():
manifest = Path(__file__).parent / "manifest.json"
with open(manifest, "r", encoding="utf-8") as file:
return json.loads(file.read())
@property
def manifest(self):
return self._manifest
@property
def hon_version(self):
return self._hon_version
@property
def pyhon_version(self):
return self._pyhon_version
class HonCoordinator(DataUpdateCoordinator):
def __init__(self, hass, device: HonAppliance):
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
name=device.unique_id,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
self._device = device
self._info = HonInfo()
async def _async_update_data(self):
await self._device.update()
@property
def info(self) -> HonInfo:
return self._info
def unique_entities(base_entities, new_entities):
result = list(base_entities) result = list(base_entities)
existing_entities = [entity.key for entity in base_entities] existing_entities = [entity.key for entity in base_entities]
entity: HonEntityDescription
for entity in new_entities: for entity in new_entities:
if entity.key not in existing_entities: if entity.key not in existing_entities:
result.append(entity) result.append(entity)
return tuple(result) return tuple(result)
def get_coordinator(hass, appliance): def get_coordinator(hass: HomeAssistantType, appliance: HonAppliance) -> HonCoordinator:
coordinators = hass.data[DOMAIN]["coordinators"] coordinators = hass.data[DOMAIN]["coordinators"]
if appliance.unique_id in coordinators: if appliance.unique_id in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][appliance.unique_id] coordinator: HonCoordinator = hass.data[DOMAIN]["coordinators"][
appliance.unique_id
]
else: else:
coordinator = HonCoordinator(hass, appliance) coordinator = HonCoordinator(hass, appliance)
hass.data[DOMAIN]["coordinators"][appliance.unique_id] = coordinator hass.data[DOMAIN]["coordinators"][appliance.unique_id] = coordinator
return coordinator return coordinator
def get_readable(description, value): def get_readable(
description: HonOptionEntityDescription, value: float | str
) -> float | str:
if description.option_list is not None: if description.option_list is not None:
with suppress(ValueError): with suppress(ValueError):
return description.option_list.get(int(value), value) return description.option_list.get(int(value), value)

View File

@ -9,6 +9,8 @@ from homeassistant.components.light import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
@ -18,7 +20,7 @@ from .hon import HonEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LIGHTS = { LIGHTS: dict[str, tuple[LightEntityDescription, ...]] = {
"WC": ( "WC": (
LightEntityDescription( LightEntityDescription(
key="settings.lightStatus", key="settings.lightStatus",
@ -43,7 +45,9 @@ LIGHTS = {
} }
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in LIGHTS.get(device.appliance_type, []): for description in LIGHTS.get(device.appliance_type, []):
@ -61,8 +65,16 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
class HonLightEntity(HonEntity, LightEntity): class HonLightEntity(HonEntity, LightEntity):
entity_description: LightEntityDescription entity_description: LightEntityDescription
def __init__(self, hass, entry, device: HonAppliance, description) -> None: def __init__(
light: HonParameterRange = device.settings.get(description.key) self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: LightEntityDescription,
) -> None:
light = self._device.settings.get(self.entity_description.key)
if not isinstance(light, HonParameterRange):
raise ValueError()
self._light_range = (light.min, light.max) self._light_range = (light.min, light.max)
self._attr_supported_color_modes: set[ColorMode] = set() self._attr_supported_color_modes: set[ColorMode] = set()
if len(light.values) == 2: if len(light.values) == 2:
@ -76,13 +88,13 @@ class HonLightEntity(HonEntity, LightEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if light is on.""" """Return true if light is on."""
return self._device.get(self.entity_description.key.split(".")[-1]) > 0 return bool(self._device.get(self.entity_description.key.split(".")[-1]) > 0)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on or control the light.""" """Turn on or control the light."""
light: HonParameterRange = self._device.settings.get( light = self._device.settings.get(self.entity_description.key)
self.entity_description.key if not isinstance(light, HonParameterRange):
) raise ValueError()
if ColorMode.BRIGHTNESS in self._attr_supported_color_modes: if ColorMode.BRIGHTNESS in self._attr_supported_color_modes:
percent = int(100 / 255 * kwargs.get(ATTR_BRIGHTNESS, 128)) percent = int(100 / 255 * kwargs.get(ATTR_BRIGHTNESS, 128))
light.value = round(light.max / 100 * percent) light.value = round(light.max / 100 * percent)
@ -96,9 +108,9 @@ class HonLightEntity(HonEntity, LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
light: HonParameterRange = self._device.settings.get( light = self._device.settings.get(self.entity_description.key)
self.entity_description.key if not isinstance(light, HonParameterRange):
) raise ValueError()
light.value = light.min light.value = light.min
await self._device.commands[self._command].send() await self._device.commands[self._command].send()
self.async_write_ha_state() self.async_write_ha_state()
@ -106,15 +118,15 @@ class HonLightEntity(HonEntity, LightEntity):
@property @property
def brightness(self) -> int | None: def brightness(self) -> int | None:
"""Return the brightness of the light.""" """Return the brightness of the light."""
light: HonParameterRange = self._device.settings.get( light = self._device.settings.get(self.entity_description.key)
self.entity_description.key if not isinstance(light, HonParameterRange):
) raise ValueError()
if light.value == light.min: if light.value == light.min:
return None return None
return int(255 / light.max * light.value) return int(255 / light.max * float(light.value))
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_is_on = self.is_on self._attr_is_on = self.is_on
self._attr_brightness = self.brightness self._attr_brightness = self.brightness
if update: if update:
@ -122,7 +134,6 @@ class HonLightEntity(HonEntity, LightEntity):
@property @property
def available(self) -> bool: def available(self) -> bool:
return ( if (entity := self._device.settings.get(self.entity_description.key)) is None:
super().available return False
and len(self._device.settings.get(self.entity_description.key).values) > 1 return super().available and len(entity.values) > 1
)

View File

@ -4,6 +4,8 @@ from typing import Any
from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.parameter.base import HonParameter from pyhon.parameter.base import HonParameter
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
@ -23,7 +25,9 @@ LOCKS: dict[str, tuple[LockEntityDescription, ...]] = {
} }
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in LOCKS.get(device.appliance_type, []): for description in LOCKS.get(device.appliance_type, []):
@ -45,13 +49,12 @@ class HonLockEntity(HonEntity, LockEntity):
@property @property
def is_locked(self) -> bool | None: def is_locked(self) -> bool | None:
"""Return a boolean for the state of the lock.""" """Return a boolean for the state of the lock."""
"""Return True if entity is on.""" return bool(self._device.get(self.entity_description.key, 0) == 1)
return self._device.get(self.entity_description.key, 0) == 1
async def async_lock(self, **kwargs: Any) -> None: async def async_lock(self, **kwargs: Any) -> None:
"""Lock method.""" """Lock method."""
setting = self._device.settings[f"settings.{self.entity_description.key}"] setting = self._device.settings.get(f"settings.{self.entity_description.key}")
if type(setting) == HonParameter: if type(setting) == HonParameter or setting is None:
return return
setting.value = setting.max if isinstance(setting, HonParameterRange) else 1 setting.value = setting.max if isinstance(setting, HonParameterRange) else 1
self.async_write_ha_state() self.async_write_ha_state()
@ -78,8 +81,7 @@ class HonLockEntity(HonEntity, LockEntity):
) )
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
value = self._device.get(self.entity_description.key, 0)
self._attr_is_locked = self.is_locked self._attr_is_locked = self.is_locked
if update: if update:
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -10,6 +10,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime, UnitOfTemperature from homeassistant.const import UnitOfTime, UnitOfTemperature
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.appliance import HonAppliance
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
from .const import DOMAIN from .const import DOMAIN
@ -183,8 +186,11 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
NUMBERS["WD"] = unique_entities(NUMBERS["WM"], NUMBERS["TD"]) NUMBERS["WD"] = unique_entities(NUMBERS["WM"], NUMBERS["TD"])
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
entity: HonNumberEntity | HonConfigNumberEntity
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in NUMBERS.get(device.appliance_type, []): for description in NUMBERS.get(device.appliance_type, []):
if description.key not in device.available_settings: if description.key not in device.available_settings:
@ -203,7 +209,13 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
class HonNumberEntity(HonEntity, NumberEntity): class HonNumberEntity(HonEntity, NumberEntity):
entity_description: HonNumberEntityDescription entity_description: HonNumberEntityDescription
def __init__(self, hass, entry, device, description) -> None: def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: HonNumberEntityDescription,
) -> None:
super().__init__(hass, entry, device, description) super().__init__(hass, entry, device, description)
self._data = device.settings[description.key] self._data = device.settings[description.key]
@ -214,7 +226,9 @@ class HonNumberEntity(HonEntity, NumberEntity):
@property @property
def native_value(self) -> float | None: def native_value(self) -> float | None:
return self._device.get(self.entity_description.key.split(".")[-1]) if value := self._device.get(self.entity_description.key.split(".")[-1]):
return float(value)
return None
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
setting = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
@ -227,7 +241,7 @@ class HonNumberEntity(HonEntity, NumberEntity):
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
setting = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
if isinstance(setting, HonParameterRange): if isinstance(setting, HonParameterRange):
self._attr_native_max_value = setting.max self._attr_native_max_value = setting.max
@ -247,14 +261,31 @@ class HonNumberEntity(HonEntity, NumberEntity):
) )
class HonConfigNumberEntity(HonNumberEntity): class HonConfigNumberEntity(HonEntity, NumberEntity):
entity_description: HonConfigNumberEntityDescription entity_description: HonConfigNumberEntityDescription
def __init__(
self,
hass: HomeAssistantType,
entry: ConfigEntry,
device: HonAppliance,
description: HonConfigNumberEntityDescription,
) -> None:
super().__init__(hass, entry, device, description)
self._data = device.settings[description.key]
if isinstance(self._data, HonParameterRange):
self._attr_native_max_value = self._data.max
self._attr_native_min_value = self._data.min
self._attr_native_step = self._data.step
@property @property
def native_value(self) -> float | None: def native_value(self) -> float | None:
return self._device.settings[self.entity_description.key].value if value := self._device.settings[self.entity_description.key].value:
return float(value)
return None
async def async_set_native_value(self, value: str) -> None: async def async_set_native_value(self, value: float) -> None:
setting = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
if isinstance(setting, HonParameterRange): if isinstance(setting, HonParameterRange):
setting.value = value setting.value = value
@ -264,3 +295,14 @@ class HonConfigNumberEntity(HonNumberEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return super(NumberEntity, self).available return super(NumberEntity, self).available
@callback
def _handle_coordinator_update(self, update: bool = True) -> None:
setting = self._device.settings[self.entity_description.key]
if isinstance(setting, HonParameterRange):
self._attr_native_max_value = setting.max
self._attr_native_min_value = setting.min
self._attr_native_step = setting.step
self._attr_native_value = self.native_value
if update:
self.async_write_ha_state()

View File

@ -2,13 +2,14 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature, UnitOfTime, REVOLUTIONS_PER_MINUTE from homeassistant.const import UnitOfTemperature, UnitOfTime, REVOLUTIONS_PER_MINUTE
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from . import const from . import const
from .const import DOMAIN from .const import DOMAIN
@ -19,16 +20,16 @@ _LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
class HonSelectEntityDescription(SelectEntityDescription): class HonSelectEntityDescription(SelectEntityDescription):
option_list: Dict[int, str] = None option_list: dict[int, str] | None = None
@dataclass @dataclass
class HonConfigSelectEntityDescription(SelectEntityDescription): class HonConfigSelectEntityDescription(SelectEntityDescription):
entity_category: EntityCategory = EntityCategory.CONFIG entity_category: EntityCategory = EntityCategory.CONFIG
option_list: Dict[int, str] = None option_list: dict[int, str] | None = None
SELECTS = { SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = {
"WM": ( "WM": (
HonConfigSelectEntityDescription( HonConfigSelectEntityDescription(
key="startProgram.spinSpeed", key="startProgram.spinSpeed",
@ -168,8 +169,11 @@ SELECTS = {
SELECTS["WD"] = unique_entities(SELECTS["WM"], SELECTS["TD"]) SELECTS["WD"] = unique_entities(SELECTS["WM"], SELECTS["TD"])
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
entity: HonSelectEntity | HonConfigSelectEntity
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in SELECTS.get(device.appliance_type, []): for description in SELECTS.get(device.appliance_type, []):
if description.key not in device.available_settings: if description.key not in device.available_settings:
@ -195,16 +199,18 @@ class HonConfigSelectEntity(HonEntity, SelectEntity):
value = get_readable(self.entity_description, setting.value) value = get_readable(self.entity_description, setting.value)
if value not in self._attr_options: if value not in self._attr_options:
return None return None
return value return str(value)
@property @property
def options(self) -> list[str]: def options(self) -> list[str]:
setting = self._device.settings.get(self.entity_description.key) setting = self._device.settings.get(self.entity_description.key)
if setting is None: if setting is None:
return [] return []
return [get_readable(self.entity_description, key) for key in setting.values] return [
str(get_readable(self.entity_description, key)) for key in setting.values
]
def _option_to_number(self, option: str, values: List[str]): def _option_to_number(self, option: str, values: list[str]) -> str:
if (options := self.entity_description.option_list) is not None: if (options := self.entity_description.option_list) is not None:
return str( return str(
next( next(
@ -220,7 +226,7 @@ class HonConfigSelectEntity(HonEntity, SelectEntity):
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_available = self.available self._attr_available = self.available
self._attr_options = self.options self._attr_options = self.options
self._attr_current_option = self.current_option self._attr_current_option = self.current_option
@ -233,9 +239,37 @@ class HonConfigSelectEntity(HonEntity, SelectEntity):
return self._device.settings.get(self.entity_description.key) is not None return self._device.settings.get(self.entity_description.key) is not None
class HonSelectEntity(HonConfigSelectEntity): class HonSelectEntity(HonEntity, SelectEntity):
entity_description: HonSelectEntityDescription entity_description: HonSelectEntityDescription
@property
def current_option(self) -> str | None:
if not (setting := self._device.settings.get(self.entity_description.key)):
return None
value = get_readable(self.entity_description, setting.value)
if value not in self._attr_options:
return None
return str(value)
@property
def options(self) -> list[str]:
setting = self._device.settings.get(self.entity_description.key)
if setting is None:
return []
return [
str(get_readable(self.entity_description, key)) for key in setting.values
]
def _option_to_number(self, option: str, values: list[str]) -> str:
if (options := self.entity_description.option_list) is not None:
return str(
next(
(k for k, v in options.items() if str(k) in values and v == option),
option,
)
)
return option
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
setting = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
setting.value = self._option_to_number(option, setting.values) setting.value = self._option_to_number(option, setting.values)
@ -253,3 +287,11 @@ class HonSelectEntity(HonConfigSelectEntity):
and int(self._device.get("remoteCtrValid", 1)) == 1 and int(self._device.get("remoteCtrValid", 1)) == 1
and self._device.get("attributes.lastConnEvent.category") != "DISCONNECTED" and self._device.get("attributes.lastConnEvent.category") != "DISCONNECTED"
) )
@callback
def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_available = self.available
self._attr_options = self.options
self._attr_current_option = self.current_option
if update:
self.async_write_ha_state()

View File

@ -1,6 +1,5 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
@ -25,6 +24,8 @@ from homeassistant.const import (
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from . import const from . import const
from .const import DOMAIN from .const import DOMAIN
@ -36,12 +37,12 @@ _LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
class HonConfigSensorEntityDescription(SensorEntityDescription): class HonConfigSensorEntityDescription(SensorEntityDescription):
entity_category: EntityCategory = EntityCategory.CONFIG entity_category: EntityCategory = EntityCategory.CONFIG
option_list: Dict[int, str] = None option_list: dict[int, str] | None = None
@dataclass @dataclass
class HonSensorEntityDescription(SensorEntityDescription): class HonSensorEntityDescription(SensorEntityDescription):
option_list: Dict[int, str] = None option_list: dict[int, str] | None = None
SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
@ -775,8 +776,11 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
SENSORS["WD"] = unique_entities(SENSORS["WM"], SENSORS["TD"]) SENSORS["WD"] = unique_entities(SENSORS["WM"], SENSORS["TD"])
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
entity: HonSensorEntity | HonConfigSensorEntity
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in SENSORS.get(device.appliance_type, []): for description in SENSORS.get(device.appliance_type, []):
if isinstance(description, HonSensorEntityDescription): if isinstance(description, HonSensorEntityDescription):
@ -799,15 +803,15 @@ class HonSensorEntity(HonEntity, SensorEntity):
entity_description: HonSensorEntityDescription entity_description: HonSensorEntityDescription
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
value = self._device.get(self.entity_description.key, "") value = self._device.get(self.entity_description.key, "")
if self.entity_description.key == "programName": if self.entity_description.key == "programName":
self._attr_options = self._device.settings.get( if not (options := self._device.settings.get("startProgram.program")):
"startProgram.program" raise ValueError
).values + ["No Program"] self._attr_options = options.values + ["No Program"]
elif self.entity_description.option_list is not None: elif self.entity_description.option_list is not None:
self._attr_options = list(self.entity_description.option_list.values()) self._attr_options = list(self.entity_description.option_list.values())
value = get_readable(self.entity_description, value) value = str(get_readable(self.entity_description, value))
if not value and self.entity_description.state_class is not None: if not value and self.entity_description.state_class is not None:
self._attr_native_value = 0 self._attr_native_value = 0
self._attr_native_value = value self._attr_native_value = value
@ -819,17 +823,22 @@ class HonConfigSensorEntity(HonEntity, SensorEntity):
entity_description: HonConfigSensorEntityDescription entity_description: HonConfigSensorEntityDescription
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
value = self._device.settings.get(self.entity_description.key, None) sensor = self._device.settings.get(self.entity_description.key, None)
value: float | str
if self.entity_description.state_class is not None: if self.entity_description.state_class is not None:
if value and value.value: if sensor and sensor.value:
value = ( value = (
float(value.value) if "." in str(value.value) else int(value.value) float(sensor.value)
if "." in str(sensor.value)
else int(sensor.value)
) )
else: else:
value = 0 value = 0
elif sensor is not None:
value = sensor.value
else: else:
value = value.value value = 0
if self.entity_description.option_list is not None and not value == 0: if self.entity_description.option_list is not None and not value == 0:
self._attr_options = list(self.entity_description.option_list.values()) self._attr_options = list(self.entity_description.option_list.values())
value = get_readable(self.entity_description, value) value = get_readable(self.entity_description, value)

View File

@ -7,6 +7,8 @@ from homeassistant.components.switch import SwitchEntityDescription, SwitchEntit
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import HomeAssistantType
from pyhon.parameter.base import HonParameter from pyhon.parameter.base import HonParameter
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
@ -17,18 +19,11 @@ _LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
class HonSwitchEntityDescriptionMixin: class HonControlSwitchEntityDescription(SwitchEntityDescription):
turn_on_key: str = "" turn_on_key: str = ""
turn_off_key: str = "" turn_off_key: str = ""
@dataclass
class HonControlSwitchEntityDescription(
HonSwitchEntityDescriptionMixin, SwitchEntityDescription
):
pass
class HonSwitchEntityDescription(SwitchEntityDescription): class HonSwitchEntityDescription(SwitchEntityDescription):
pass pass
@ -38,7 +33,7 @@ class HonConfigSwitchEntityDescription(SwitchEntityDescription):
entity_category: EntityCategory = EntityCategory.CONFIG entity_category: EntityCategory = EntityCategory.CONFIG
SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = { SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
"WM": ( "WM": (
HonControlSwitchEntityDescription( HonControlSwitchEntityDescription(
key="active", key="active",
@ -355,8 +350,11 @@ SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["WM"])
SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["TD"]) SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["TD"])
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
entities = [] entities = []
entity: HonConfigSwitchEntity | HonControlSwitchEntity | HonSwitchEntity
for device in hass.data[DOMAIN][entry.unique_id].appliances: for device in hass.data[DOMAIN][entry.unique_id].appliances:
for description in SWITCHES.get(device.appliance_type, []): for description in SWITCHES.get(device.appliance_type, []):
if isinstance(description, HonConfigSwitchEntityDescription): if isinstance(description, HonConfigSwitchEntityDescription):
@ -427,7 +425,7 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
return True return True
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_is_on = self.is_on self._attr_is_on = self.is_on
if update: if update:
self.async_write_ha_state() self.async_write_ha_state()
@ -507,7 +505,7 @@ class HonConfigSwitchEntity(HonEntity, SwitchEntity):
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@callback @callback
def _handle_coordinator_update(self, update=True) -> None: def _handle_coordinator_update(self, update: bool = True) -> None:
self._attr_is_on = self.is_on self._attr_is_on = self.is_on
if update: if update:
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -0,0 +1,95 @@
from typing import Union, TypeVar
from homeassistant.components.button import ButtonEntityDescription
from homeassistant.components.fan import FanEntityDescription
from homeassistant.components.light import LightEntityDescription
from homeassistant.components.lock import LockEntityDescription
from homeassistant.components.number import NumberEntityDescription
from homeassistant.components.select import SelectEntityDescription
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.components.switch import SwitchEntityDescription
from .binary_sensor import HonBinarySensorEntityDescription
from .button import HonButtonEntity, HonDataArchive, HonDeviceInfo
from .climate import (
HonACClimateEntityDescription,
HonClimateEntityDescription,
)
from .number import (
HonConfigNumberEntityDescription,
HonNumberEntityDescription,
)
from .select import (
HonConfigSelectEntityDescription,
HonSelectEntityDescription,
)
from .sensor import (
HonSensorEntityDescription,
HonConfigSensorEntityDescription,
)
from .switch import (
HonControlSwitchEntityDescription,
HonSwitchEntityDescription,
HonConfigSwitchEntityDescription,
)
HonButtonType = Union[
HonButtonEntity,
HonDataArchive,
HonDeviceInfo,
]
HonEntityDescription = Union[
HonBinarySensorEntityDescription,
HonControlSwitchEntityDescription,
HonSwitchEntityDescription,
HonConfigSwitchEntityDescription,
HonSensorEntityDescription,
HonConfigSelectEntityDescription,
HonConfigNumberEntityDescription,
HonACClimateEntityDescription,
HonClimateEntityDescription,
HonNumberEntityDescription,
HonSelectEntityDescription,
HonConfigSensorEntityDescription,
FanEntityDescription,
LightEntityDescription,
LockEntityDescription,
ButtonEntityDescription,
SwitchEntityDescription,
SensorEntityDescription,
SelectEntityDescription,
NumberEntityDescription,
]
HonOptionEntityDescription = Union[
HonConfigSelectEntityDescription,
HonSelectEntityDescription,
HonConfigSensorEntityDescription,
HonSensorEntityDescription,
]
T = TypeVar(
"T",
HonBinarySensorEntityDescription,
HonControlSwitchEntityDescription,
HonSwitchEntityDescription,
HonConfigSwitchEntityDescription,
HonSensorEntityDescription,
HonConfigSelectEntityDescription,
HonConfigNumberEntityDescription,
HonACClimateEntityDescription,
HonClimateEntityDescription,
HonNumberEntityDescription,
HonSelectEntityDescription,
HonConfigSensorEntityDescription,
FanEntityDescription,
LightEntityDescription,
LockEntityDescription,
ButtonEntityDescription,
SwitchEntityDescription,
SensorEntityDescription,
SelectEntityDescription,
NumberEntityDescription,
)

25
mypy.ini Normal file
View File

@ -0,0 +1,25 @@
[mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
disable_error_code = annotation-unchecked
enable_error_code = ignore-without-code, redundant-self, truthy-iterable
follow_imports = silent
local_partial_types = true
no_implicit_optional = true
no_implicit_reexport = true
show_error_codes = true
strict_concatenate = false
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
[mypy-homeassistant.*]
implicit_reexport = True

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyhOn
homeassistant

View File

@ -1,3 +1,4 @@
pyhOn
black black
homeassistant flake8
mypy
pylint