first commit
This commit is contained in:
commit
31c30b1670
12 changed files with 450 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__/
|
||||||
|
test.py
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# hOn
|
||||||
|
Home Assistant component supporting hOn cloud.
|
52
custom_components/hon/__init__.py
Normal file
52
custom_components/hon/__init__.py
Normal file
|
@ -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()
|
42
custom_components/hon/config_flow.py
Executable file
42
custom_components/hon/config_flow.py
Executable file
|
@ -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)
|
7
custom_components/hon/const.py
Executable file
7
custom_components/hon/const.py
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
DOMAIN = "hon"
|
||||||
|
|
||||||
|
PLATFORMS = [
|
||||||
|
"sensor",
|
||||||
|
"select",
|
||||||
|
"number"
|
||||||
|
]
|
29
custom_components/hon/hon.py
Executable file
29
custom_components/hon/hon.py
Executable file
|
@ -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,
|
||||||
|
)
|
8
custom_components/hon/manifest.json
Executable file
8
custom_components/hon/manifest.json
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"domain": "hon",
|
||||||
|
"name": "hOn",
|
||||||
|
"config_flow": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"codeowners": ["@Andre0512"],
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
}
|
100
custom_components/hon/number.py
Normal file
100
custom_components/hon/number.py
Normal file
|
@ -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()
|
95
custom_components/hon/select.py
Normal file
95
custom_components/hon/select.py
Normal file
|
@ -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()
|
93
custom_components/hon/sensor.py
Normal file
93
custom_components/hon/sensor.py
Normal file
|
@ -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()
|
14
custom_components/hon/translations/en.json
Normal file
14
custom_components/hon/translations/en.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "hOn",
|
||||||
|
"description": "Please enters your hOn credentials",
|
||||||
|
"data": {
|
||||||
|
"email": "Email Address",
|
||||||
|
"password": "Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
hacs.json
Normal file
6
hacs.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Haier hOn",
|
||||||
|
"content_in_root": true,
|
||||||
|
"render_readme": false,
|
||||||
|
"homeassistant": "2023.2.0"
|
||||||
|
}
|
Loading…
Reference in a new issue