Compare commits

..

20 Commits

Author SHA1 Message Date
92add01a59 Bump pyhon version 2023-04-11 01:03:54 +02:00
4a685e67e0 Try to fix login issues #11 2023-04-10 20:35:22 +02:00
6303843116 Add python check action 2023-04-10 19:59:04 +02:00
907bc44533 Reformat with black 2023-04-10 19:51:16 +02:00
4901be4050 Update badges, bump version 2023-04-10 18:50:51 +02:00
c8189414b8 Use pyhon v0.6.1 2023-04-10 17:02:16 +02:00
799ac67d94 Use info.md for hacs 2023-04-10 08:17:27 +02:00
7e9202ef38 New pyhOn version 2023-04-10 07:09:54 +02:00
e1a2af70e9 Bump to v0.4.0 2023-04-08 06:15:33 +02:00
c78aeb1fbb Merge pull request #14 from MiguelAngelLV/oven
Add oven support
2023-04-08 04:46:45 +02:00
67f280512d Merge branch 'main' into oven 2023-04-08 04:45:56 +02:00
6093b57f76 Merge branch 'main' into oven 2023-04-08 04:44:47 +02:00
287e895f4d Merge branch 'main' into oven 2023-04-08 04:33:55 +02:00
d3dc1b9f45 Merge pull request #13 from alexandre-leites/main
Added WD device type sensors.
2023-04-07 20:56:44 +02:00
116f9d5470 Add oven support 2023-04-07 13:52:55 +02:00
05e9d835b1 Added WD device type sensors. 2023-04-05 23:11:19 +02:00
135d6cafed Update to for hacs default integration 2023-04-02 02:44:13 +02:00
077bded6dd Improve washing machine sensors 2023-04-02 02:44:13 +02:00
0d575f65f9 Merge pull request #7 from drudgebg/main
Adding time selection for some of the TD programs
2023-03-22 22:33:19 +01:00
34e888f6d6 Adding time selection for some of the TD programs 2023-03-22 09:25:30 +02:00
16 changed files with 507 additions and 138 deletions

38
.github/workflows/python_check.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Python check
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pylint black
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
# - name: Analysing the code with pylint
# run: |
# pylint --max-line-length 88 $(git ls-files '*.py')
- name: Check black style
run: |
black . --check

View File

@ -1,24 +1,30 @@
# Haier hOn
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)
Home Assistant component supporting devices of Haier's mobile app **hOn**.
[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://hacs.xyz)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Andre0512/hon?color=green)](https://github.com/Andre0512/hon/releases/latest)
![GitHub](https://img.shields.io/github/license/Andre0512/hon?color=red)
[![Home Assistant installs](https://img.shields.io/badge/dynamic/json?color=blue&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.hon.total)](https://analytics.home-assistant.io/)
Home Assistant integration for Haier hOn: support for Haier/Candy/Hoover home appliances like washing machines.
## Supported Appliances
- Washing Machine
- Tumble Dryer
- Washer Dryer
- Washing Machine
- Oven
## Installation
#### Installing via HACS
1. You need to have installed [HACS](https://hacs.xyz/)
2. Go to HACS->Integrations
3. Add this repo (`https://github.com/Andre0512/hon.git`) into your HACS custom repositories
4. Search for Haier hOn and Download it
5. Restart your HomeAssistant
6. Go to Settings->Devices & Services
7. Shift reload your browser
8. Click Add Integration
9. Search for Haier hOn
10. Type your username used in the hOn App and hit submit
**Method 1:** [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Andre0512&repository=hon&category=integration)
**Method 2:** [HACS](https://hacs.xyz/) > Integrations > Add Integration > **Haier hOn** > Install
**Method 3:** Manually copy `hon` folder from [latest release](https://github.com/Andre0512/hon/releases/latest) to `config/custom_components` folder.
_Restart Home Assistant_
## Configuration
**Method 1**: [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=hon)
**Method 2**: Settings > Devices & Services > Add Integration > **Haier hOn**
_If the integration is not in the list, you need to clear the browser cache._
## Contribute
Any kind of contribution is welcome!
@ -62,12 +68,15 @@ Any kind of contribution is welcome!
5. Create a [pull request](https://github.com/Andre0512/hon/pulls)
#### Tips and Tricks
- If you want to have some states humanreadable, have a look at the `translation_key` parameter of the `EntityDescription`
- If you want to have some states humanreadable, have a look at the `translation_key` parameter of the `EntityDescription`.
- If you need to implement some more logic, create a pull request to the underlying library. There we collect special requirements in the `appliances` directory.
- Use [pyhOn's translate command](https://github.com/Andre0512/pyhOn#translation) to read out the official translations
## Tested Devices
- Haier WD90-B14TEAM5
- Haier HD80-A3959
- Haier HWO60SM2F3XH
- Hoover H-WASH 500
## About this Repo
The existing integrations missed some features from the app I liked to have in HomeAssistant.

View File

@ -1,7 +1,7 @@
import logging
import voluptuous as vol
from pyhon import HonConnection
from pyhon import Hon
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
@ -28,8 +28,9 @@ CONFIG_SCHEMA = vol.Schema(
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()
hon = await Hon(
entry.data["email"], entry.data["password"], session=session
).create()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = hon
hass.data[DOMAIN]["coordinators"] = {}

View File

@ -1,10 +1,13 @@
import logging
from dataclasses import dataclass
from pyhon import HonConnection
from pyhon import Hon
from homeassistant.components.binary_sensor import BinarySensorEntityDescription, BinarySensorDeviceClass, \
BinarySensorEntity
from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from .const import DOMAIN
@ -19,7 +22,9 @@ class HonBinarySensorEntityDescriptionMixin:
@dataclass
class HonBinarySensorEntityDescription(HonBinarySensorEntityDescriptionMixin, BinarySensorEntityDescription):
class HonBinarySensorEntityDescription(
HonBinarySensorEntityDescriptionMixin, BinarySensorEntityDescription
):
pass
@ -27,15 +32,22 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
"WM": (
HonBinarySensorEntityDescription(
key="attributes.lastConnEvent.category",
name="Connection",
name="Remote Control",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED",
icon="mdi:remote",
),
HonBinarySensorEntityDescription(
key="doorLockStatus",
name="Door Lock",
device_class=BinarySensorDeviceClass.LOCK,
on_value="0",
),
HonBinarySensorEntityDescription(
key="doorStatus",
name="Door",
device_class=BinarySensorDeviceClass.DOOR,
on_value="0",
on_value="1",
),
),
"TD": (
@ -51,15 +63,75 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
device_class=BinarySensorDeviceClass.DOOR,
on_value="1",
),
)
),
"WD": (
HonBinarySensorEntityDescription(
key="attributes.lastConnEvent.category",
name="Remote Control",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED",
icon="mdi:remote",
),
HonBinarySensorEntityDescription(
key="startProgram.prewash",
name="Pre Wash",
),
HonBinarySensorEntityDescription(
key="extraRinse1",
name="Extra Rinse 1",
),
HonBinarySensorEntityDescription(
key="extraRinse2",
name="Extra Rinse 2",
),
HonBinarySensorEntityDescription(
key="extraRinse3",
name="Extra Rinse 3",
),
HonBinarySensorEntityDescription(
key="goodNight",
name="Good Night Mode",
),
HonBinarySensorEntityDescription(
key="acquaplus",
name="Acqua Plus",
),
HonBinarySensorEntityDescription(
key="anticrease",
name="Anti-Crease",
),
),
"OV": (
HonBinarySensorEntityDescription(
key="attributes.lastConnEvent.category",
name="Online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED",
icon="mdi:wifi",
),
HonBinarySensorEntityDescription(
key="attributes.parameters.remoteCtrValid",
name="On",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="1",
icon="mdi:remote",
),
HonBinarySensorEntityDescription(
key="attributes.parameters.onOffStatus",
name="On",
device_class=BinarySensorDeviceClass.RUNNING,
on_value="1",
icon="mdi:power-cycle",
),
),
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -70,10 +142,16 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := BINARY_SENSORS.get(device.appliance_type):
for description in descriptions:
if not device.get(description.key):
_LOGGER.info("Can't setup %s", description.key)
_LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
continue
appliances.extend([
HonBinarySensorEntity(hass, coordinator, entry, device, description)]
appliances.extend(
[
HonBinarySensorEntity(
hass, coordinator, entry, device, description
)
]
)
async_add_entities(appliances)
@ -92,9 +170,15 @@ class HonBinarySensorEntity(HonEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
return self._device.get(self.entity_description.key, "") == self.entity_description.on_value
return (
self._device.get(self.entity_description.key, "")
== self.entity_description.on_value
)
@callback
def _handle_coordinator_update(self):
self._attr_native_value = self._device.get(self.entity_description.key, "") == self.entity_description.on_value
self._attr_native_value = (
self._device.get(self.entity_description.key, "")
== self.entity_description.on_value
)
self.async_write_ha_state()

View File

@ -1,33 +1,32 @@
from pyhon import HonConnection
from pyhon.device import HonDevice
from homeassistant.components.button import ButtonEntityDescription, ButtonEntity
from homeassistant.config_entries import ConfigEntry
from pyhon import Hon
from pyhon.appliance import HonAppliance
from .const import DOMAIN
from .hon import HonCoordinator, HonEntity
BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = {
"WM": (
# ButtonEntityDescription(
# key="pauseProgram",
# name="Pause Program",
# icon="mdi:pause",
# ),
# ButtonEntityDescription(
# key="resumeProgram",
# name="Resume Program",
# icon="mdi:play-pause",
# ),
"OV": (
ButtonEntityDescription(
key="startProgram",
name="Start Program",
icon="mdi:power-cycle",
),
ButtonEntityDescription(
key="stopProgram",
name="Stop Program",
icon="mdi:power-off",
),
)
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -39,15 +38,17 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
for description in descriptions:
if not device.commands.get(description.key):
continue
appliances.extend([
HonButtonEntity(hass, coordinator, entry, device, description)]
appliances.extend(
[HonButtonEntity(hass, coordinator, entry, device, description)]
)
async_add_entities(appliances)
class HonButtonEntity(HonEntity, ButtonEntity):
def __init__(self, hass, coordinator, entry, device: HonDevice, description) -> None:
def __init__(
self, hass, coordinator, entry, device: HonAppliance, description
) -> None:
super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator

View File

@ -20,8 +20,12 @@ class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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}))
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]

View File

@ -1,7 +1,7 @@
import logging
from datetime import timedelta
from pyhon.device import HonDevice
from pyhon.appliance import HonAppliance
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
class HonEntity(CoordinatorEntity):
_attr_has_entity_name = True
def __init__(self, hass, entry, coordinator, device: HonDevice) -> None:
def __init__(self, hass, entry, coordinator, device: HonAppliance) -> None:
super().__init__(coordinator)
self._hon = hass.data[DOMAIN][entry.unique_id]
@ -29,16 +29,23 @@ class HonEntity(CoordinatorEntity):
return DeviceInfo(
identifiers={(DOMAIN, self._device.mac_address)},
manufacturer=self._device.get("brand", ""),
name=self._device.nick_name if self._device.nick_name else self._device.model_name,
name=self._device.nick_name
if self._device.nick_name
else self._device.model_name,
model=self._device.model_name,
sw_version=self._device.get("fwVersion", ""),
)
class HonCoordinator(DataUpdateCoordinator):
def __init__(self, hass, device: HonDevice):
def __init__(self, hass, device: HonAppliance):
"""Initialize my coordinator."""
super().__init__(hass, _LOGGER, name=device.mac_address, update_interval=timedelta(seconds=30))
super().__init__(
hass,
_LOGGER,
name=device.mac_address,
update_interval=timedelta(seconds=30),
)
self._device = device
async def _async_update_data(self):

View File

@ -6,7 +6,6 @@
"documentation": "https://github.com/Andre0512/hon/",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/Andre0512/hon/issues",
"requirements": ["pyhOn==0.4.1"],
"version": "0.3.0"
"requirements": ["pyhOn==0.6.4"],
"version": "0.5.0-beta.5"
}

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from pyhon import HonConnection
from pyhon import Hon
from pyhon.parameter import HonParameterRange
from homeassistant.components.number import (
@ -8,7 +8,7 @@ from homeassistant.components.number import (
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
from homeassistant.const import UnitOfTime, UnitOfTemperature
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory
@ -22,20 +22,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay Time",
icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES
native_unit_of_measurement=UnitOfTime.MINUTES,
),
NumberEntityDescription(
key="startProgram.rinseIterations",
name="Rinse Iterations",
icon="mdi:rotate-right",
entity_category=EntityCategory.CONFIG
entity_category=EntityCategory.CONFIG,
),
NumberEntityDescription(
key="startProgram.mainWashTime",
name="Main Wash Time",
icon="mdi:clock-start",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES
native_unit_of_measurement=UnitOfTime.MINUTES,
),
),
"TD": (
@ -44,44 +44,76 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay time",
icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES
native_unit_of_measurement=UnitOfTime.MINUTES,
),
NumberEntityDescription(
key="startProgram.dryLevel",
name="Dry level",
entity_category=EntityCategory.CONFIG,
icon="mdi:hair-dryer",
translation_key="tumbledryerdrylevel"
translation_key="tumbledryerdrylevel",
),
NumberEntityDescription(
key="startProgram.tempLevel",
name="Temperature level",
entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer",
translation_key="tumbledryertemplevel"
translation_key="tumbledryertemplevel",
),
NumberEntityDescription(
key="startProgram.antiCreaseTime",
name="Anti-Crease time",
entity_category=EntityCategory.CONFIG,
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES
native_unit_of_measurement=UnitOfTime.MINUTES,
),
NumberEntityDescription(
key="startProgram.sterilizationStatus",
name="Sterilization status",
icon="mdi:clock-start",
entity_category=EntityCategory.CONFIG
entity_category=EntityCategory.CONFIG,
),
),
"WD": (
NumberEntityDescription(
key="startProgram.delayTime",
name="Delay Time",
icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES,
),
),
"OV": (
NumberEntityDescription(
key="startProgram.delayTime",
name="Delay time",
icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES,
),
NumberEntityDescription(
key="startProgram.tempSel",
name="Target Temperature",
entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
NumberEntityDescription(
key="startProgram.prTime",
name="Program Duration",
entity_category=EntityCategory.CONFIG,
icon="mdi:timelapse",
native_unit_of_measurement=UnitOfTime.MINUTES,
),
),
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -91,8 +123,10 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := NUMBERS.get(device.appliance_type):
for description in descriptions:
appliances.extend([
HonNumberEntity(hass, coordinator, entry, device, description)]
if not device.settings.get(description.key):
continue
appliances.extend(
[HonNumberEntity(hass, coordinator, entry, device, description)]
)
async_add_entities(appliances)

View File

@ -1,18 +1,22 @@
from __future__ import annotations
from pyhon import HonConnection
from pyhon.device import HonDevice
import logging
from pyhon import Hon
from pyhon.appliance import HonAppliance
from pyhon.parameter import HonParameterFixed
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature, REVOLUTIONS_PER_MINUTE
from homeassistant.const import UnitOfTemperature, UnitOfTime, REVOLUTIONS_PER_MINUTE
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory
from .const import DOMAIN
from .hon import HonEntity, HonCoordinator
_LOGGER = logging.getLogger(__name__)
SELECTS = {
"WM": (
SelectEntityDescription(
@ -20,20 +24,20 @@ SELECTS = {
name="Spin speed",
entity_category=EntityCategory.CONFIG,
icon="mdi:numeric",
unit_of_measurement=REVOLUTIONS_PER_MINUTE
unit_of_measurement=REVOLUTIONS_PER_MINUTE,
),
SelectEntityDescription(
key="startProgram.temp",
name="Temperature",
entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer",
unit_of_measurement=UnitOfTemperature.CELSIUS
unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SelectEntityDescription(
key="startProgram.program",
name="Program",
entity_category=EntityCategory.CONFIG,
translation_key="programs"
translation_key="programs",
),
),
"TD": (
@ -41,17 +45,44 @@ SELECTS = {
key="startProgram.program",
name="Program",
entity_category=EntityCategory.CONFIG,
translation_key="programs"
translation_key="programs",
),
SelectEntityDescription(
key="startProgram.dryTimeMM",
name="Time",
entity_category=EntityCategory.CONFIG,
icon="mdi:timer",
unit_of_measurement=UnitOfTime.MINUTES,
),
),
"WD": (
SelectEntityDescription(
key="startProgram.program",
name="Program",
entity_category=EntityCategory.CONFIG,
translation_key="programs",
),
),
"OV": (
SelectEntityDescription(
key="startProgram.program",
name="Program",
entity_category=EntityCategory.CONFIG,
),
SelectEntityDescription(
key="startProgram.preheatStatus",
name="Preheat",
entity_category=EntityCategory.CONFIG,
),
),
)
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -61,16 +92,18 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := SELECTS.get(device.appliance_type):
for description in descriptions:
if not device.get(description.key):
if not device.settings.get(description.key):
continue
appliances.extend([
HonSelectEntity(hass, coordinator, entry, device, description)]
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:
def __init__(
self, hass, coordinator, entry, device: HonAppliance, description
) -> None:
super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator
@ -97,7 +130,9 @@ class HonSelectEntity(HonEntity, SelectEntity):
@callback
def _handle_coordinator_update(self):
setting = self._device.settings[self.entity_description.key]
if not isinstance(self._device.settings[self.entity_description.key], HonParameterFixed):
if not isinstance(
self._device.settings[self.entity_description.key], HonParameterFixed
):
self._attr_options: list[str] = setting.values
else:
self._attr_options = [setting.value]

View File

@ -1,6 +1,6 @@
import logging
from pyhon import HonConnection
from pyhon import Hon
from homeassistant.components.sensor import (
SensorEntity,
@ -9,7 +9,15 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfVolume, UnitOfMass, UnitOfPower, UnitOfTime
from homeassistant.const import (
REVOLUTIONS_PER_MINUTE,
UnitOfEnergy,
UnitOfVolume,
UnitOfMass,
UnitOfPower,
UnitOfTime,
UnitOfTemperature,
)
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.typing import StateType
@ -26,20 +34,20 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
name="Total Power",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="totalWaterUsed",
name="Total Water",
device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfVolume.LITERS
native_unit_of_measurement=UnitOfVolume.LITERS,
),
SensorEntityDescription(
key="totalWashCycle",
name="Total Wash Cycle",
state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:counter"
icon="mdi:counter",
),
SensorEntityDescription(
key="currentElectricityUsed",
@ -47,13 +55,13 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
icon="mdi:lightning-bolt"
icon="mdi:lightning-bolt",
),
SensorEntityDescription(
key="currentWaterUsed",
name="Current Water Used",
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:water"
icon="mdi:water",
),
SensorEntityDescription(
key="startProgram.weight",
@ -61,19 +69,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
icon="mdi:weight-kilogram"
icon="mdi:weight-kilogram",
),
SensorEntityDescription(
key="machMode",
name="Machine Status",
icon="mdi:information",
translation_key="mode"
translation_key="mode",
),
SensorEntityDescription(
key="errors",
name="Error",
icon="mdi:math-log",
translation_key="errors"
key="errors", name="Error", icon="mdi:math-log", translation_key="errors"
),
SensorEntityDescription(
key="remainingTimeMM",
@ -85,7 +90,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
SensorEntityDescription(
key="spinSpeed",
name="Spin Speed",
icon="mdi:timer",
icon="mdi:speedometer",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
),
@ -95,13 +100,10 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
key="machMode",
name="Machine Status",
icon="mdi:information",
translation_key="mode"
translation_key="mode",
),
SensorEntityDescription(
key="errors",
name="Error",
icon="mdi:math-log",
translation_key="errors"
key="errors", name="Error", icon="mdi:math-log", translation_key="errors"
),
SensorEntityDescription(
key="remainingTimeMM",
@ -121,35 +123,112 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
key="prCode",
name="Program",
icon="mdi:tumble-dryer",
translation_key="tumbledryerprogram"
translation_key="tumbledryerprogram",
),
SensorEntityDescription(
key="prPhase",
name="Program Phase",
icon="mdi:tumble-dryer",
translation_key="tumbledryerprogramphase"
translation_key="tumbledryerprogramphase",
),
SensorEntityDescription(
key="dryLevel",
name="Dry level",
icon="mdi:hair-dryer",
translation_key="tumbledryerdrylevel"
translation_key="tumbledryerdrylevel",
),
SensorEntityDescription(
key="tempLevel",
name="Temperature level",
icon="mdi:thermometer",
translation_key="tumbledryertemplevel"
translation_key="tumbledryertemplevel",
),
),
"WD": (
SensorEntityDescription(
key="machMode",
name="Machine Status",
icon="mdi:information",
translation_key="mode",
),
SensorEntityDescription(
key="spinSpeed",
name="Spin Speed",
icon="mdi:fast-forward-outline",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
),
SensorEntityDescription(
key="remainingTimeMM",
name="Remaining Time",
icon="mdi:timer",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
),
SensorEntityDescription(
key="prCode",
name="Current Program",
icon="mdi:tumble-dryer",
),
SensorEntityDescription(
key="prPhase",
name="Program Phase",
icon="mdi:tumble-dryer",
),
SensorEntityDescription(
key="dryLevel",
name="Dry level",
icon="mdi:hair-dryer",
),
SensorEntityDescription(
key="dirtyLevel",
name="Dirt level",
icon="mdi:liquid-spot",
),
SensorEntityDescription(
key="steamLevel",
name="Steam level",
icon="mdi:smoke",
),
SensorEntityDescription(
key="temp",
name="Current Temperature",
icon="mdi:thermometer",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
),
"OV": (
SensorEntityDescription(
key="remainingTimeMM",
name="Remaining Time",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES,
),
SensorEntityDescription(
key="delayTime",
name="Start Time",
icon="mdi:clock-start",
),
SensorEntityDescription(
key="temp",
name="Temperature",
icon="mdi:thermometer",
),
SensorEntityDescription(
key="tempSel",
name="Temperature Selected",
icon="mdi:thermometer",
),
),
)
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -160,9 +239,12 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := SENSORS.get(device.appliance_type):
for description in descriptions:
if not device.get(description.key):
_LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
continue
appliances.extend([
HonSensorEntity(hass, coordinator, entry, device, description)]
appliances.extend(
[HonSensorEntity(hass, coordinator, entry, device, description)]
)
async_add_entities(appliances)

View File

@ -1,15 +1,19 @@
import logging
from dataclasses import dataclass
from typing import Any
from homeassistant.components.switch import SwitchEntityDescription, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from pyhon import HonConnection
from pyhon.device import HonDevice
from pyhon import Hon
from pyhon.appliance import HonAppliance
from .const import DOMAIN
from .hon import HonCoordinator, HonEntity
_LOGGER = logging.getLogger(__name__)
@dataclass
class HonSwitchEntityDescriptionMixin:
@ -18,8 +22,8 @@ class HonSwitchEntityDescriptionMixin:
@dataclass
class HonSwitchEntityDescription(HonSwitchEntityDescriptionMixin,
SwitchEntityDescription
class HonSwitchEntityDescription(
HonSwitchEntityDescriptionMixin, SwitchEntityDescription
):
pass
@ -44,13 +48,13 @@ SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = {
key="startProgram.delayStatus",
name="Delay Status",
icon="mdi:timer-check",
entity_category=EntityCategory.CONFIG
entity_category=EntityCategory.CONFIG,
),
HonSwitchEntityDescription(
key="startProgram.haier_SoakPrewashSelection",
name="Soak Prewash Selection",
icon="mdi:tshirt-crew",
entity_category=EntityCategory.CONFIG
entity_category=EntityCategory.CONFIG,
),
),
"TD": (
@ -69,14 +73,30 @@ SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = {
turn_off_key="resumeProgram",
),
),
"WD": (
HonSwitchEntityDescription(
key="active",
name="Washing Machine",
icon="mdi:washing-machine",
turn_on_key="startProgram",
turn_off_key="stopProgram",
),
HonSwitchEntityDescription(
key="pause",
name="Pause Washing Machine",
icon="mdi:pause",
turn_on_key="pauseProgram",
turn_off_key="resumeProgram",
),
),
}
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None:
hon: HonConnection = hass.data[DOMAIN][entry.unique_id]
hon: Hon = hass.data[DOMAIN][entry.unique_id]
coordinators = hass.data[DOMAIN]["coordinators"]
appliances = []
for device in hon.devices:
for device in hon.appliances:
if device.mac_address in coordinators:
coordinator = hass.data[DOMAIN]["coordinators"][device.mac_address]
else:
@ -86,9 +106,16 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := SWITCHES.get(device.appliance_type):
for description in descriptions:
if device.get(description.key) is not None or device.commands.get(description.key) is not None:
appliances.extend([
HonSwitchEntity(hass, coordinator, entry, device, description)]
if (
device.get(description.key) is not None
or device.commands.get(description.key) is not None
):
appliances.extend(
[HonSwitchEntity(hass, coordinator, entry, device, description)]
)
else:
_LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
async_add_entities(appliances)
@ -97,7 +124,14 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
class HonSwitchEntity(HonEntity, SwitchEntity):
entity_description: HonSwitchEntityDescription
def __init__(self, hass, coordinator, entry, device: HonDevice, description: HonSwitchEntityDescription) -> None:
def __init__(
self,
hass,
coordinator,
entry,
device: HonAppliance,
description: HonSwitchEntityDescription,
) -> None:
super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator
self._device = device
@ -106,7 +140,9 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
def available(self) -> bool:
if self.entity_category == EntityCategory.CONFIG:
return self._device.settings[self.entity_description.key].typology != "fixed"
return (
self._device.settings[self.entity_description.key].typology != "fixed"
)
return True
@property
@ -114,7 +150,11 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
"""Return True if entity is on."""
if self.entity_category == EntityCategory.CONFIG:
setting = self._device.settings[self.entity_description.key]
return setting.value == "1" or hasattr(setting, "min") and setting.value != setting.min
return (
setting.value == "1"
or hasattr(setting, "min")
and setting.value != setting.min
)
return self._device.get(self.entity_description.key, False)
async def async_turn_on(self, **kwargs: Any) -> None:
@ -132,5 +172,3 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
self.async_write_ha_state()
else:
await self._device.commands[self.entity_description.turn_off_key].send()

View File

@ -377,6 +377,7 @@
"single_item_steam": "Single Item + Steam",
"smart_wash": "Smart Wash",
"soft_care": "Soft Care",
"soft_care_steam": "Soft Care + Steam",
"soft_care_steam_title": "Soft Care + Steam",
"special_39": "Special 39'",
"special_39_full_load": "Special 39'",

View File

@ -377,6 +377,7 @@
"single_item_steam": "Single Item + Steam",
"smart_wash": "Smart Wash",
"soft_care": "Soft Care",
"soft_care_steam": "Soft Care + Steam",
"soft_care_steam_title": "Soft Care + Steam",
"special_39": "Special 39'",
"special_39_full_load": "Special 39'",

View File

@ -1,5 +1,4 @@
{
"name": "Haier hOn",
"render_readme": true,
"homeassistant": "2023.2.0"
}

36
info.md Normal file
View File

@ -0,0 +1,36 @@
# Haier hOn
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Andre0512/hon?color=green)](https://github.com/Andre0512/hon/releases/latest)
![GitHub](https://img.shields.io/github/license/Andre0512/hon?color=red)
[![Home Assistant installs](https://img.shields.io/badge/dynamic/json?color=blue&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.hon.total)](https://analytics.home-assistant.io/)
Support for home appliances of Haier's mobile app hOn.
## Supported Appliances
- Tumble Dryer
- Washer Dryer
- Washing Machine
- Oven
## Tested Appliances
- Haier WD90-B14TEAM5
- Haier HD80-A3959
- Haier HWO60SM2F3XH
- Hoover H-WASH 500
## Configuration
**Method 1**: [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=hon)
**Method 2**: Settings > Devices & Services > Add Integration > **Haier hOn**
_If the integration is not in the list, you need to clear the browser cache._
## Contribute
Want to help us to support more appliances?
Or add more sensors?
Or help with translating?
Or beautify some icons or captions?
Check out the [project on GitHub](https://github.com/Andre0512/hon), every contribution is welcome!