Compare commits

...

10 Commits

Author SHA1 Message Date
593842144a Add zip release for download count 2023-04-12 23:59:09 +02:00
9a6e1155f9 Add useful links 2023-04-12 23:58:10 +02:00
365a3af171 Clean up sessions correct, fixes #19 2023-04-12 19:28:04 +02:00
6de6ff375c Bump version to v0.5.0 2023-04-12 02:11:41 +02:00
89d2fd4af1 Read out device data via ui 2023-04-11 22:08:47 +02:00
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
15 changed files with 262 additions and 100 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

25
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Release
on:
release:
types: [published]
jobs:
release-zip:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: ZIP Component Dir
run: |
cd ${{ github.workspace }}/custom_components/hon
zip -r haier_hon.zip ./
- name: Upload zip to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ github.workspace }}/custom_components/hon/haier_hon.zip
asset_name: haier_hon.zip
tag: ${{ github.ref }}
overwrite: true

View File

@ -1,7 +1,8 @@
# Haier hOn # Haier hOn
[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://hacs.xyz)
[![GitHub manifest version (path)](https://img.shields.io/github/manifest-json/v/andre0512/hon?color=g&filename=custom_components%2Fhon%2Fmanifest.json)](https://github.com/Andre0512/hon/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Andre0512/hon?color=green)](https://github.com/Andre0512/hon/releases/latest)
[![Home Assistant installs](https://img.shields.io/badge/dynamic/json?color=41BDF5&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.hon.total)](https://analytics.home-assistant.io/) ![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. Home Assistant integration for Haier hOn: support for Haier/Candy/Hoover home appliances like washing machines.
## Supported Appliances ## Supported Appliances
- Tumble Dryer - Tumble Dryer
@ -9,6 +10,7 @@ Home Assistant integration for Haier hOn: support for Haier/Candy/Hoover home ap
- Washing Machine - Washing Machine
- Oven - Oven
## Installation ## Installation
**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 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)
@ -25,14 +27,34 @@ _Restart Home Assistant_
**Method 2**: Settings > Devices & Services > Add Integration > **Haier 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._ _If the integration is not in the list, you need to clear the browser cache._
## Contribute ## Contribute
Any kind of contribution is welcome! Any kind of contribution is welcome!
#### Add appliances or additional attributes ### Read out device data
If you want to make a request for adding new appliances or additional attributes and don't want to use the command line, here is how you can read out your device data.
For every device exists a hidden button which can be used to log all info of your appliance.
1. Enable the "Log Device Info" button
_This button can be found in the diagnostic section of your device or in the entity overview if "show disabled entities" is enabled._
2. Press the button
3. Go to Settings > System > Logs, click _load full logs_ and scroll down
_The formatting is messy if you not load full logs_
4. Here you can find all data which can be read out via the api
```yaml
data:
appliance:
applianceId: 12-34-56-78-90-ab#2022-10-25T19:47:11Z
applianceModelId: 1569
...
```
5. Copy this data and create a [new issue](https://github.com/Andre0512/hon/issues/new) with your request
### Add appliances or additional attributes
1. Install [pyhOn](https://github.com/Andre0512/pyhOn) 1. Install [pyhOn](https://github.com/Andre0512/pyhOn)
```commandline ```commandline
$ pip install pyhOn $ pip install pyhOn
``` ```
2. Use the commandline tool to read out all appliance data from your account 2. Use the command line tool to read out all appliance data from your account
```commandline ```commandline
$ pyhOn $ pyhOn
User for hOn account: user.name@example.com User for hOn account: user.name@example.com
@ -69,7 +91,7 @@ Any kind of contribution is welcome!
#### Tips and Tricks #### 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. - 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](https://github.com/Andre0512/pyhOn)s translate command to read out the official translations - Use [pyhOn's translate command](https://github.com/Andre0512/pyhOn#translation) to read out the official translations
## Tested Devices ## Tested Devices
- Haier WD90-B14TEAM5 - Haier WD90-B14TEAM5

View File

@ -28,7 +28,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
hon = await Hon(entry.data["email"], entry.data["password"], session=session).create() hon = await Hon(
entry.data["email"], entry.data["password"], session=session
).create()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = hon hass.data[DOMAIN][entry.unique_id] = hon
hass.data[DOMAIN]["coordinators"] = {} hass.data[DOMAIN]["coordinators"] = {}

View File

@ -3,8 +3,11 @@ from dataclasses import dataclass
from pyhon import Hon from pyhon import Hon
from homeassistant.components.binary_sensor import BinarySensorEntityDescription, BinarySensorDeviceClass, \ from homeassistant.components.binary_sensor import (
BinarySensorEntity BinarySensorEntityDescription,
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from .const import DOMAIN from .const import DOMAIN
@ -19,7 +22,9 @@ class HonBinarySensorEntityDescriptionMixin:
@dataclass @dataclass
class HonBinarySensorEntityDescription(HonBinarySensorEntityDescriptionMixin, BinarySensorEntityDescription): class HonBinarySensorEntityDescription(
HonBinarySensorEntityDescriptionMixin, BinarySensorEntityDescription
):
pass pass
@ -30,7 +35,7 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
name="Remote Control", name="Remote Control",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED", on_value="CONNECTED",
icon="mdi:remote" icon="mdi:remote",
), ),
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="doorLockStatus", key="doorLockStatus",
@ -65,7 +70,7 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
name="Remote Control", name="Remote Control",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED", on_value="CONNECTED",
icon="mdi:remote" icon="mdi:remote",
), ),
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="startProgram.prewash", key="startProgram.prewash",
@ -102,21 +107,21 @@ BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
name="Online", name="Online",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED", on_value="CONNECTED",
icon="mdi:wifi" icon="mdi:wifi",
), ),
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="attributes.parameters.remoteCtrValid", key="attributes.parameters.remoteCtrValid",
name="On", name="On",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="1", on_value="1",
icon="mdi:remote" icon="mdi:remote",
), ),
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="attributes.parameters.onOffStatus", key="attributes.parameters.onOffStatus",
name="On", name="On",
device_class=BinarySensorDeviceClass.RUNNING, device_class=BinarySensorDeviceClass.RUNNING,
on_value="1", on_value="1",
icon="mdi:power-cycle" icon="mdi:power-cycle",
), ),
), ),
} }
@ -137,10 +142,16 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := BINARY_SENSORS.get(device.appliance_type): if descriptions := BINARY_SENSORS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.get(description.key): if not device.get(description.key):
_LOGGER.warning("[%s] Can't setup %s", device.appliance_type, description.key) _LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
continue continue
appliances.extend([ appliances.extend(
HonBinarySensorEntity(hass, coordinator, entry, device, description)] [
HonBinarySensorEntity(
hass, coordinator, entry, device, description
)
]
) )
async_add_entities(appliances) async_add_entities(appliances)
@ -159,9 +170,15 @@ class HonBinarySensorEntity(HonEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: 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 @callback
def _handle_coordinator_update(self): 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() self.async_write_ha_state()

View File

@ -1,11 +1,18 @@
import logging
import urllib
from urllib.parse import quote
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 pyhon import Hon from pyhon import Hon
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from homeassistant.const import EntityCategory
from .const import DOMAIN from .const import DOMAIN
from .hon import HonCoordinator, HonEntity from .hon import HonCoordinator, HonEntity
_LOGGER = logging.getLogger(__name__)
BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = {
"OV": ( "OV": (
ButtonEntityDescription( ButtonEntityDescription(
@ -38,15 +45,18 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
for description in descriptions: for description in descriptions:
if not device.commands.get(description.key): if not device.commands.get(description.key):
continue continue
appliances.extend([ appliances.extend(
HonButtonEntity(hass, coordinator, entry, device, description)] [HonButtonEntity(hass, coordinator, entry, device, description)]
) )
appliances.extend([HonFeatureRequestButton(hass, coordinator, entry, device)])
async_add_entities(appliances) async_add_entities(appliances)
class HonButtonEntity(HonEntity, ButtonEntity): class HonButtonEntity(HonEntity, ButtonEntity):
def __init__(self, hass, coordinator, entry, device: HonAppliance, description) -> None: def __init__(
self, hass, coordinator, entry, device: HonAppliance, description
) -> None:
super().__init__(hass, entry, coordinator, device) super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator self._coordinator = coordinator
@ -56,3 +66,18 @@ class HonButtonEntity(HonEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
await self._device.commands[self.entity_description.key].send() await self._device.commands[self.entity_description.key].send()
class HonFeatureRequestButton(HonEntity, ButtonEntity):
def __init__(self, hass, coordinator, entry, device: HonAppliance) -> None:
super().__init__(hass, entry, coordinator, device)
self._device = device
self._attr_unique_id = f"{super().unique_id}_log_device_info"
self._attr_icon = "mdi:information"
self._attr_name = "Log Device Info"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_entity_registry_enabled_default = False
async def async_press(self) -> None:
_LOGGER.error("Device Info:\n" + self._device.diagnose)

View File

@ -20,8 +20,12 @@ class HonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
if user_input is None: if user_input is None:
return self.async_show_form(step_id="user", data_schema=vol.Schema( return self.async_show_form(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str})) step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
),
)
self._email = user_input[CONF_EMAIL] self._email = user_input[CONF_EMAIL]
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]

View File

@ -29,7 +29,9 @@ class HonEntity(CoordinatorEntity):
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device.mac_address)}, identifiers={(DOMAIN, self._device.mac_address)},
manufacturer=self._device.get("brand", ""), 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, model=self._device.model_name,
sw_version=self._device.get("fwVersion", ""), sw_version=self._device.get("fwVersion", ""),
) )
@ -38,7 +40,12 @@ class HonEntity(CoordinatorEntity):
class HonCoordinator(DataUpdateCoordinator): class HonCoordinator(DataUpdateCoordinator):
def __init__(self, hass, device: HonAppliance): def __init__(self, hass, device: HonAppliance):
"""Initialize my coordinator.""" """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 self._device = device
async def _async_update_data(self): async def _async_update_data(self):

View File

@ -6,7 +6,6 @@
"documentation": "https://github.com/Andre0512/hon/", "documentation": "https://github.com/Andre0512/hon/",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/Andre0512/hon/issues", "issue_tracker": "https://github.com/Andre0512/hon/issues",
"requirements": ["pyhOn==0.6.1"], "requirements": ["pyhOn==0.7.3"],
"version": "0.5.0-beta.2" "version": "0.5.1-beta.1"
} }

View File

@ -22,20 +22,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay Time", name="Delay Time",
icon="mdi:timer-plus", icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.rinseIterations", key="startProgram.rinseIterations",
name="Rinse Iterations", name="Rinse Iterations",
icon="mdi:rotate-right", icon="mdi:rotate-right",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.mainWashTime", key="startProgram.mainWashTime",
name="Main Wash Time", name="Main Wash Time",
icon="mdi:clock-start", icon="mdi:clock-start",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
), ),
"TD": ( "TD": (
@ -44,34 +44,34 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay time", name="Delay time",
icon="mdi:timer-plus", icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.dryLevel", key="startProgram.dryLevel",
name="Dry level", name="Dry level",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:hair-dryer", icon="mdi:hair-dryer",
translation_key="tumbledryerdrylevel" translation_key="tumbledryerdrylevel",
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.tempLevel", key="startProgram.tempLevel",
name="Temperature level", name="Temperature level",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer", icon="mdi:thermometer",
translation_key="tumbledryertemplevel" translation_key="tumbledryertemplevel",
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.antiCreaseTime", key="startProgram.antiCreaseTime",
name="Anti-Crease time", name="Anti-Crease time",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:timer", icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.sterilizationStatus", key="startProgram.sterilizationStatus",
name="Sterilization status", name="Sterilization status",
icon="mdi:clock-start", icon="mdi:clock-start",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG,
), ),
), ),
"WD": ( "WD": (
@ -80,7 +80,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay Time", name="Delay Time",
icon="mdi:timer-plus", icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
), ),
"OV": ( "OV": (
@ -89,22 +89,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
name="Delay time", name="Delay time",
icon="mdi:timer-plus", icon="mdi:timer-plus",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.tempSel", key="startProgram.tempSel",
name="Target Temperature", name="Target Temperature",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer", icon="mdi:thermometer",
native_unit_of_measurement=UnitOfTemperature.CELSIUS native_unit_of_measurement=UnitOfTemperature.CELSIUS,
), ),
NumberEntityDescription( NumberEntityDescription(
key="startProgram.prTime", key="startProgram.prTime",
name="Program Duration", name="Program Duration",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:timelapse", icon="mdi:timelapse",
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES,
), ),
), ),
} }
@ -126,8 +125,8 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
for description in descriptions: for description in descriptions:
if not device.settings.get(description.key): if not device.settings.get(description.key):
continue continue
appliances.extend([ appliances.extend(
HonNumberEntity(hass, coordinator, entry, device, description)] [HonNumberEntity(hass, coordinator, entry, device, description)]
) )
async_add_entities(appliances) async_add_entities(appliances)

View File

@ -24,20 +24,20 @@ SELECTS = {
name="Spin speed", name="Spin speed",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:numeric", icon="mdi:numeric",
unit_of_measurement=REVOLUTIONS_PER_MINUTE unit_of_measurement=REVOLUTIONS_PER_MINUTE,
), ),
SelectEntityDescription( SelectEntityDescription(
key="startProgram.temp", key="startProgram.temp",
name="Temperature", name="Temperature",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:thermometer", icon="mdi:thermometer",
unit_of_measurement=UnitOfTemperature.CELSIUS unit_of_measurement=UnitOfTemperature.CELSIUS,
), ),
SelectEntityDescription( SelectEntityDescription(
key="startProgram.program", key="startProgram.program",
name="Program", name="Program",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="programs" translation_key="programs",
), ),
), ),
"TD": ( "TD": (
@ -45,14 +45,14 @@ SELECTS = {
key="startProgram.program", key="startProgram.program",
name="Program", name="Program",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="programs" translation_key="programs",
), ),
SelectEntityDescription( SelectEntityDescription(
key="startProgram.dryTimeMM", key="startProgram.dryTimeMM",
name="Time", name="Time",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
icon="mdi:timer", icon="mdi:timer",
unit_of_measurement=UnitOfTime.MINUTES unit_of_measurement=UnitOfTime.MINUTES,
), ),
), ),
"WD": ( "WD": (
@ -60,7 +60,7 @@ SELECTS = {
key="startProgram.program", key="startProgram.program",
name="Program", name="Program",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="programs" translation_key="programs",
), ),
), ),
"OV": ( "OV": (
@ -72,7 +72,7 @@ SELECTS = {
SelectEntityDescription( SelectEntityDescription(
key="startProgram.preheatStatus", key="startProgram.preheatStatus",
name="Preheat", name="Preheat",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG,
), ),
), ),
} }
@ -94,14 +94,16 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
for description in descriptions: for description in descriptions:
if not device.settings.get(description.key): if not device.settings.get(description.key):
continue continue
appliances.extend([ appliances.extend(
HonSelectEntity(hass, coordinator, entry, device, description)] [HonSelectEntity(hass, coordinator, entry, device, description)]
) )
async_add_entities(appliances) async_add_entities(appliances)
class HonSelectEntity(HonEntity, SelectEntity): class HonSelectEntity(HonEntity, SelectEntity):
def __init__(self, hass, coordinator, entry, device: HonAppliance, description) -> None: def __init__(
self, hass, coordinator, entry, device: HonAppliance, description
) -> None:
super().__init__(hass, entry, coordinator, device) super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator self._coordinator = coordinator
@ -128,7 +130,9 @@ class HonSelectEntity(HonEntity, SelectEntity):
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
setting = self._device.settings[self.entity_description.key] 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 self._attr_options: list[str] = setting.values
else: else:
self._attr_options = [setting.value] self._attr_options = [setting.value]

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
UnitOfMass, UnitOfMass,
UnitOfPower, UnitOfPower,
UnitOfTime, UnitOfTime,
UnitOfTemperature UnitOfTemperature,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
@ -34,20 +34,20 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
name="Total Power", name="Total Power",
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
), ),
SensorEntityDescription( SensorEntityDescription(
key="totalWaterUsed", key="totalWaterUsed",
name="Total Water", name="Total Water",
device_class=SensorDeviceClass.WATER, device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfVolume.LITERS native_unit_of_measurement=UnitOfVolume.LITERS,
), ),
SensorEntityDescription( SensorEntityDescription(
key="totalWashCycle", key="totalWashCycle",
name="Total Wash Cycle", name="Total Wash Cycle",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
icon="mdi:counter" icon="mdi:counter",
), ),
SensorEntityDescription( SensorEntityDescription(
key="currentElectricityUsed", key="currentElectricityUsed",
@ -55,13 +55,13 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT, native_unit_of_measurement=UnitOfPower.KILO_WATT,
icon="mdi:lightning-bolt" icon="mdi:lightning-bolt",
), ),
SensorEntityDescription( SensorEntityDescription(
key="currentWaterUsed", key="currentWaterUsed",
name="Current Water Used", name="Current Water Used",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
icon="mdi:water" icon="mdi:water",
), ),
SensorEntityDescription( SensorEntityDescription(
key="startProgram.weight", key="startProgram.weight",
@ -69,19 +69,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfMass.KILOGRAMS, native_unit_of_measurement=UnitOfMass.KILOGRAMS,
icon="mdi:weight-kilogram" icon="mdi:weight-kilogram",
), ),
SensorEntityDescription( SensorEntityDescription(
key="machMode", key="machMode",
name="Machine Status", name="Machine Status",
icon="mdi:information", icon="mdi:information",
translation_key="mode" translation_key="mode",
), ),
SensorEntityDescription( SensorEntityDescription(
key="errors", key="errors", name="Error", icon="mdi:math-log", translation_key="errors"
name="Error",
icon="mdi:math-log",
translation_key="errors"
), ),
SensorEntityDescription( SensorEntityDescription(
key="remainingTimeMM", key="remainingTimeMM",
@ -103,13 +100,10 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
key="machMode", key="machMode",
name="Machine Status", name="Machine Status",
icon="mdi:information", icon="mdi:information",
translation_key="mode" translation_key="mode",
), ),
SensorEntityDescription( SensorEntityDescription(
key="errors", key="errors", name="Error", icon="mdi:math-log", translation_key="errors"
name="Error",
icon="mdi:math-log",
translation_key="errors"
), ),
SensorEntityDescription( SensorEntityDescription(
key="remainingTimeMM", key="remainingTimeMM",
@ -129,25 +123,25 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
key="prCode", key="prCode",
name="Program", name="Program",
icon="mdi:tumble-dryer", icon="mdi:tumble-dryer",
translation_key="tumbledryerprogram" translation_key="tumbledryerprogram",
), ),
SensorEntityDescription( SensorEntityDescription(
key="prPhase", key="prPhase",
name="Program Phase", name="Program Phase",
icon="mdi:tumble-dryer", icon="mdi:tumble-dryer",
translation_key="tumbledryerprogramphase" translation_key="tumbledryerprogramphase",
), ),
SensorEntityDescription( SensorEntityDescription(
key="dryLevel", key="dryLevel",
name="Dry level", name="Dry level",
icon="mdi:hair-dryer", icon="mdi:hair-dryer",
translation_key="tumbledryerdrylevel" translation_key="tumbledryerdrylevel",
), ),
SensorEntityDescription( SensorEntityDescription(
key="tempLevel", key="tempLevel",
name="Temperature level", name="Temperature level",
icon="mdi:thermometer", icon="mdi:thermometer",
translation_key="tumbledryertemplevel" translation_key="tumbledryertemplevel",
), ),
), ),
"WD": ( "WD": (
@ -155,7 +149,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
key="machMode", key="machMode",
name="Machine Status", name="Machine Status",
icon="mdi:information", icon="mdi:information",
translation_key="mode" translation_key="mode",
), ),
SensorEntityDescription( SensorEntityDescription(
key="spinSpeed", key="spinSpeed",
@ -245,10 +239,12 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := SENSORS.get(device.appliance_type): if descriptions := SENSORS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.get(description.key): if not device.get(description.key):
_LOGGER.warning("[%s] Can't setup %s", device.appliance_type, description.key) _LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
continue continue
appliances.extend([ appliances.extend(
HonSensorEntity(hass, coordinator, entry, device, description)] [HonSensorEntity(hass, coordinator, entry, device, description)]
) )
async_add_entities(appliances) async_add_entities(appliances)

View File

@ -22,9 +22,9 @@ class HonSwitchEntityDescriptionMixin:
@dataclass @dataclass
class HonSwitchEntityDescription(HonSwitchEntityDescriptionMixin, class HonSwitchEntityDescription(
SwitchEntityDescription HonSwitchEntityDescriptionMixin, SwitchEntityDescription
): ):
pass pass
@ -48,13 +48,13 @@ SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = {
key="startProgram.delayStatus", key="startProgram.delayStatus",
name="Delay Status", name="Delay Status",
icon="mdi:timer-check", icon="mdi:timer-check",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG,
), ),
HonSwitchEntityDescription( HonSwitchEntityDescription(
key="startProgram.haier_SoakPrewashSelection", key="startProgram.haier_SoakPrewashSelection",
name="Soak Prewash Selection", name="Soak Prewash Selection",
icon="mdi:tshirt-crew", icon="mdi:tshirt-crew",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG,
), ),
), ),
"TD": ( "TD": (
@ -106,12 +106,17 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
if descriptions := SWITCHES.get(device.appliance_type): if descriptions := SWITCHES.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if device.get(description.key) is not None or device.commands.get(description.key) is not None: if (
appliances.extend([ device.get(description.key) is not None
HonSwitchEntity(hass, coordinator, entry, device, description)] or device.commands.get(description.key) is not None
):
appliances.extend(
[HonSwitchEntity(hass, coordinator, entry, device, description)]
) )
else: else:
_LOGGER.warning("[%s] Can't setup %s", device.appliance_type, description.key) _LOGGER.warning(
"[%s] Can't setup %s", device.appliance_type, description.key
)
async_add_entities(appliances) async_add_entities(appliances)
@ -119,7 +124,14 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
class HonSwitchEntity(HonEntity, SwitchEntity): class HonSwitchEntity(HonEntity, SwitchEntity):
entity_description: HonSwitchEntityDescription entity_description: HonSwitchEntityDescription
def __init__(self, hass, coordinator, entry, device: HonAppliance, description: HonSwitchEntityDescription) -> None: def __init__(
self,
hass,
coordinator,
entry,
device: HonAppliance,
description: HonSwitchEntityDescription,
) -> None:
super().__init__(hass, entry, coordinator, device) super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator self._coordinator = coordinator
self._device = device self._device = device
@ -128,7 +140,9 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
def available(self) -> bool: def available(self) -> bool:
if self.entity_category == EntityCategory.CONFIG: 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 return True
@property @property
@ -136,7 +150,11 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
"""Return True if entity is on.""" """Return True if entity is on."""
if self.entity_category == EntityCategory.CONFIG: if self.entity_category == EntityCategory.CONFIG:
setting = self._device.settings[self.entity_description.key] 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) return self._device.get(self.entity_description.key, False)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -1,4 +1,6 @@
{ {
"name": "Haier hOn", "name": "Haier hOn",
"homeassistant": "2023.2.0" "homeassistant": "2023.2.0",
"zip_release": true,
"filename": "haier_hon.zip"
} }

16
info.md
View File

@ -1,6 +1,7 @@
# Haier hOn # Haier hOn
[![GitHub manifest version (path)](https://img.shields.io/github/manifest-json/v/andre0512/hon?color=g&filename=custom_components%2Fhon%2Fmanifest.json)](https://github.com/Andre0512/hon/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Andre0512/hon?color=green)](https://github.com/Andre0512/hon/releases/latest)
[![Home Assistant installs](https://img.shields.io/badge/dynamic/json?color=41BDF5&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.hon.total)](https://analytics.home-assistant.io/) ![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. Support for home appliances of Haier's mobile app hOn.
## Supported Appliances ## Supported Appliances
@ -25,11 +26,14 @@ _If the integration is not in the list, you need to clear the browser cache._
## Contribute ## Contribute
Want to help us to support more appliances? Want to help us to support more appliances? Or add more sensors? Or help with translating? Or beautify some icons or captions?
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! Check out the [project on GitHub](https://github.com/Andre0512/hon), every contribution is welcome!
## Useful Links
* [GitHub repository](https://github.com/Andre0512/hon) (please add a star if you like this integration!)
* [pyhOn library](https://github.com/Andre0512/pyhOn)
* [Release notes](https://github.com/Andre0512/hon/releases)
* [Discussion and help](https://github.com/Andre0512/hon/discussions)
* [Issues](https://github.com/Andre0512/hon/issues)