From 31c30b1670ad03078706641862200d3f9ac44e89 Mon Sep 17 00:00:00 2001 From: Andre Basche Date: Sun, 19 Feb 2023 02:58:21 +0100 Subject: [PATCH] first commit --- .gitignore | 2 + README.md | 2 + custom_components/hon/__init__.py | 52 +++++++++++ custom_components/hon/config_flow.py | 42 +++++++++ custom_components/hon/const.py | 7 ++ custom_components/hon/hon.py | 29 ++++++ custom_components/hon/manifest.json | 8 ++ custom_components/hon/number.py | 100 +++++++++++++++++++++ custom_components/hon/select.py | 95 ++++++++++++++++++++ custom_components/hon/sensor.py | 93 +++++++++++++++++++ custom_components/hon/translations/en.json | 14 +++ hacs.json | 6 ++ 12 files changed, 450 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 custom_components/hon/__init__.py create mode 100755 custom_components/hon/config_flow.py create mode 100755 custom_components/hon/const.py create mode 100755 custom_components/hon/hon.py create mode 100755 custom_components/hon/manifest.json create mode 100644 custom_components/hon/number.py create mode 100644 custom_components/hon/select.py create mode 100644 custom_components/hon/sensor.py create mode 100644 custom_components/hon/translations/en.json create mode 100644 hacs.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1f7009 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +test.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e8b26b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# hOn +Home Assistant component supporting hOn cloud. diff --git a/custom_components/hon/__init__.py b/custom_components/hon/__init__.py new file mode 100644 index 0000000..66c60b7 --- /dev/null +++ b/custom_components/hon/__init__.py @@ -0,0 +1,52 @@ +import logging +from datetime import timedelta + +import voluptuous as vol +from pyhon import HonConnection +from pyhon.device import HonDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from custom_components.hon.const import DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +HON_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [HON_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + session = aiohttp_client.async_get_clientsession(hass) + hon = HonConnection(entry.data["email"], entry.data["password"], session) + await hon.setup() + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.unique_id] = hon + hass.data[DOMAIN]["coordinators"] = {} + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + +class HonCoordinator(DataUpdateCoordinator): + def __init__(self, hass, device: HonDevice): + """Initialize my coordinator.""" + super().__init__(hass, _LOGGER, name=device.mac_address, update_interval=timedelta(seconds=30)) + self._device = device + + async def _async_update_data(self): + await self._device.load_attributes() diff --git a/custom_components/hon/config_flow.py b/custom_components/hon/config_flow.py new file mode 100755 index 0000000..1a286c8 --- /dev/null +++ b/custom_components/hon/config_flow.py @@ -0,0 +1,42 @@ +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + self._email = None + self._password = None + + async def async_step_user(self, user_input=None): + if user_input is None: + return self.async_show_form(step_id="user", data_schema=vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str})) + + self._email = user_input[CONF_EMAIL] + self._password = user_input[CONF_PASSWORD] + + # Check if already configured + await self.async_set_unique_id(self._email) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self._email, + data={ + CONF_EMAIL: self._email, + CONF_PASSWORD: self._password, + }, + ) + + async def async_step_import(self, user_input=None): + return await self.async_step_user(user_input) diff --git a/custom_components/hon/const.py b/custom_components/hon/const.py new file mode 100755 index 0000000..09d08f6 --- /dev/null +++ b/custom_components/hon/const.py @@ -0,0 +1,7 @@ +DOMAIN = "hon" + +PLATFORMS = [ + "sensor", + "select", + "number" +] diff --git a/custom_components/hon/hon.py b/custom_components/hon/hon.py new file mode 100755 index 0000000..91b83ea --- /dev/null +++ b/custom_components/hon/hon.py @@ -0,0 +1,29 @@ +from pyhon.device import HonDevice + +from .const import DOMAIN +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + + +class HonEntity(CoordinatorEntity): + _attr_has_entity_name = True + + def __init__(self, hass, entry, coordinator, device: HonDevice) -> None: + super().__init__(coordinator) + + self._hon = hass.data[DOMAIN][entry.unique_id] + self._hass = hass + self._device = device + + self._attr_unique_id = self._device.mac_address + + @property + def device_info(self): + """Return a device description for device registry.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device.mac_address)}, + manufacturer=self._device.brand, + name=self._device.nick_name if self._device.nick_name else self._device.model_name, + model=self._device.model_name, + sw_version=self._device.fw_version, + ) diff --git a/custom_components/hon/manifest.json b/custom_components/hon/manifest.json new file mode 100755 index 0000000..11e4887 --- /dev/null +++ b/custom_components/hon/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "hon", + "name": "hOn", + "config_flow": true, + "version": "0.0.1", + "codeowners": ["@Andre0512"], + "iot_class": "cloud_polling" +} diff --git a/custom_components/hon/number.py b/custom_components/hon/number.py new file mode 100644 index 0000000..8172bea --- /dev/null +++ b/custom_components/hon/number.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from pyhon import HonConnection +from pyhon.parameter import HonParameterRange + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory +from custom_components import DOMAIN, HonCoordinator +from custom_components.hon import HonEntity + +NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { + "WM": ( + NumberEntityDescription( + key="delayStatus", + name="delayStatus", + entity_category=EntityCategory.CONFIG + ), + NumberEntityDescription( + key="delayTime", + name="delayTime", + icon="mdi:timer", + entity_category=EntityCategory.CONFIG + ), + NumberEntityDescription( + key="haier_SoakPrewashSelection", + name="haier_SoakPrewashSelection", + entity_category=EntityCategory.CONFIG + ), + NumberEntityDescription( + key="rinseIterations", + name="rinseIterations", + entity_category=EntityCategory.CONFIG + ), + NumberEntityDescription( + key="mainWashTime", + name="mainWashTime", + icon="mdi:timer", + entity_category=EntityCategory.CONFIG + ), + ), +} + + +async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: + hon: HonConnection = hass.data[DOMAIN][entry.unique_id] + coordinators = hass.data[DOMAIN]["coordinators"] + appliances = [] + for device in hon.devices: + if device.mac_address in coordinators: + coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address] + else: + coordinator = HonCoordinator(hass, device) + hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator + await coordinator.async_config_entry_first_refresh() + + if descriptions := NUMBERS.get(device.appliance_type_name): + for description in descriptions: + appliances.extend([ + HonNumberEntity(hass, coordinator, entry, device, description)] + ) + + async_add_entities(appliances) + + +class HonNumberEntity(HonEntity, NumberEntity): + def __init__(self, hass, coordinator, entry, device, description) -> None: + super().__init__(hass, entry, coordinator, device) + + self._coordinator = coordinator + self._data = device.settings[description.key] + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{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 + def native_value(self) -> float | None: + return self._data.value + + async def async_set_native_value(self, value: float) -> None: + self._data.value = value + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self): + self._data = self._device.settings[self.entity_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 + self._attr_native_value = self._data.value + self.async_write_ha_state() diff --git a/custom_components/hon/select.py b/custom_components/hon/select.py new file mode 100644 index 0000000..abf54c2 --- /dev/null +++ b/custom_components/hon/select.py @@ -0,0 +1,95 @@ +"""Support for Tuya select.""" +from __future__ import annotations + +from pyhon import HonConnection +from pyhon.device import HonDevice +from pyhon.parameter import HonParameterFixed + +from config.custom_components.hon import HonCoordinator +from config.custom_components.hon.hon import HonEntity +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory + +DOMAIN = "hon" + +SELECTS = { + "WM": ( + SelectEntityDescription( + key="spinSpeed", + name="Spin speed", + entity_category=EntityCategory.CONFIG, + icon="mdi:numeric" + ), + SelectEntityDescription( + key="temp", + name="Temperature", + entity_category=EntityCategory.CONFIG, + icon="mdi:thermometer" + ), + SelectEntityDescription( + key="program", + name="Programme", + entity_category=EntityCategory.CONFIG + ), + ) +} + + +async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: + hon: HonConnection = hass.data[DOMAIN][entry.unique_id] + coordinators = hass.data[DOMAIN]["coordinators"] + appliances = [] + for device in hon.devices: + if device.mac_address in coordinators: + coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address] + else: + coordinator = HonCoordinator(hass, device) + hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator + await coordinator.async_config_entry_first_refresh() + + if descriptions := SELECTS.get(device.appliance_type_name): + for description in descriptions: + appliances.extend([ + HonSelectEntity(hass, coordinator, entry, device, description)] + ) + + async_add_entities(appliances) + + +class HonSelectEntity(HonEntity, SelectEntity): + def __init__(self, hass, coordinator, entry, device: HonDevice, description) -> None: + super().__init__(hass, entry, coordinator, device) + + self._coordinator = coordinator + self._device = device + self._data = device.settings[description.key] + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + if not isinstance(self._data, HonParameterFixed): + self._attr_options: list[str] = self._data.values + else: + self._attr_options = [self._data.value] + + @property + def current_option(self) -> str | None: + value = self._data.value + if value is None or value not in self._attr_options: + return None + return value + + async def async_select_option(self, option: str) -> None: + self._data.value = option + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self): + self._data = self._device.settings[self.entity_description.key] + if not isinstance(self._data, HonParameterFixed): + self._attr_options: list[str] = self._data.values + else: + self._attr_options = [self._data.value] + self._attr_native_value = self._data.value + self.async_write_ha_state() diff --git a/custom_components/hon/sensor.py b/custom_components/hon/sensor.py new file mode 100644 index 0000000..cc6224a --- /dev/null +++ b/custom_components/hon/sensor.py @@ -0,0 +1,93 @@ +import logging + +from pyhon import HonConnection + +from homeassistant.components.sensor import ( + SensorEntity, + SensorDeviceClass, + SensorStateClass, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.typing import StateType +from custom_components import HonCoordinator +from .const import DOMAIN +from custom_components.hon import HonEntity + +_LOGGER = logging.getLogger(__name__) + +SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { + "WM": ( + SensorEntityDescription( + key="totalElectricityUsed", + name="Total Power", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="totalWaterUsed", + name="Total Water", + device_class=SensorDeviceClass.WATER, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="totalWashCycle", + name="Total Wash Cycle", + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:counter" + ), + SensorEntityDescription( + key="currentElectricityUsed", + name="Current Electricity Used", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:lightning-bolt" + ), + SensorEntityDescription( + key="currentWaterUsed", + name="Current Water Used", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:water" + ), + ) +} + + +async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: + hon: HonConnection = hass.data[DOMAIN][entry.unique_id] + coordinators = hass.data[DOMAIN]["coordinators"] + appliances = [] + for device in hon.devices: + if device.mac_address in coordinators: + coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address] + else: + coordinator = HonCoordinator(hass, device) + hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator + await coordinator.async_config_entry_first_refresh() + + if descriptions := SENSORS.get(device.appliance_type_name): + for description in descriptions: + appliances.extend([ + HonSensorEntity(hass, coordinator, entry, device, description)] + ) + + async_add_entities(appliances) + + +class HonSensorEntity(HonEntity, SensorEntity): + def __init__(self, hass, coordinator, entry, device, description) -> None: + super().__init__(hass, entry, coordinator, device) + + self._coordinator = coordinator + + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + @property + def native_value(self) -> StateType: + return self._device.attributes.get(self.entity_description.key, "") + + @callback + def _handle_coordinator_update(self): + self._attr_native_value = self._device.attributes.get(self.entity_description.key, "") + self.async_write_ha_state() diff --git a/custom_components/hon/translations/en.json b/custom_components/hon/translations/en.json new file mode 100644 index 0000000..07c0c04 --- /dev/null +++ b/custom_components/hon/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "title": "hOn", + "description": "Please enters your hOn credentials", + "data": { + "email": "Email Address", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..458c86d --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Haier hOn", + "content_in_root": true, + "render_readme": false, + "homeassistant": "2023.2.0" +} \ No newline at end of file