Compare commits

..

4 Commits

Author SHA1 Message Date
075d34b5e2 Fix many bugs 2023-03-08 23:00:55 +01:00
88c76b8056 Add contribution instructions to readme 2023-03-06 18:41:13 +01:00
03a1e40b6e Rename repo 2023-03-06 13:41:58 +01:00
9d8b17b2cf Add sensors, renamed repo 2023-03-06 00:20:52 +01:00
17 changed files with 152 additions and 79 deletions

17
.github/workflows/hacs_check.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: HACS Action
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
hacs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"

View File

@ -1,16 +1,14 @@
name: Validate name: Validate with hassfest
on: on:
push: push:
pull_request: pull_request:
schedule:
- cron: '0 0 * * *'
jobs: jobs:
validate: validate:
runs-on: ubuntu-latest runs-on: "ubuntu-latest"
steps: steps:
- uses: actions/checkout@v2 - uses: "actions/checkout@v3"
- uses: home-assistant/actions/hassfest@master - uses: "home-assistant/actions/hassfest@master"
- name: HACS validation
uses: hacs/action@main
with:
category: integration

View File

@ -1,11 +1,13 @@
# Haier hOn # Haier hOn
Home Assistant component supporting hOn cloud. [![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**.
## Installation ## Installation
#### Installing via HACS #### Installing via HACS
1. You need to have installed [HACS](https://hacs.xyz/) 1. You need to have installed [HACS](https://hacs.xyz/)
2. Go to HACS->Integrations 2. Go to HACS->Integrations
3. Add this repo (`https://github.com/Andre0512/haier.git`) into your HACS custom repositories 3. Add this repo (`https://github.com/Andre0512/hon.git`) into your HACS custom repositories
4. Search for Haier hOn and Download it 4. Search for Haier hOn and Download it
5. Restart your HomeAssistant 5. Restart your HomeAssistant
6. Go to Settings->Devices & Services 6. Go to Settings->Devices & Services
@ -14,6 +16,51 @@ Home Assistant component supporting hOn cloud.
9. Search for Haier hOn 9. Search for Haier hOn
10. Type your username used in the hOn App and hit submit 10. Type your username used in the hOn App and hit submit
## Contribute
Any kind of contribution is welcome!
#### Add appliances or additional attributes
1. Install [pyhOn](https://github.com/Andre0512/pyhOn)
```commandline
$ pip install pyhOn
```
2. Use the commandline tool to read out all appliance data from your account
```commandline
$ pyhOn
User for hOn account: user.name@example.com
Password for hOn account: ********
========== WM - Washing Machine ==========
commands:
pauseProgram: pauseProgram command
resumeProgram: resumeProgram command
startProgram: startProgram command
stopProgram: stopProgram command
data:
actualWeight: 0
airWashTempLevel: 0
airWashTime: 0
antiAllergyStatus: 0
...
```
3. Fork this repository and clone it to your local machine
4. Add the keys of the attributes you'd like to have as `EntityDescription` into this Repository
_Example: Add pause button_
```python
BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = {
"WM": ( # WM is the applianceTypeName
ButtonEntityDescription(
key="pauseProgram", # key from pyhOn
name="Pause Program", # name in home assistant
icon="mdi:pause", # icon in home assistant
...
),
...
```
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 need to implement some more logic, create a pull request to the underlying library. There we collect special requirements in the `appliances` directory.
## Supported Appliances ## Supported Appliances
- Washing Machine - Washing Machine

View File

@ -1,10 +0,0 @@
{
"domain": "haier",
"name": "Haier hOn",
"codeowners": ["@Andre0512"],
"config_flow": true,
"documentation": "https://github.com/Andre0512/haier/",
"iot_class": "cloud_polling",
"requirements": ["pyhOn==0.2.4"],
"version": "0.1.1"
}

View File

@ -26,14 +26,14 @@ class HonBinarySensorEntityDescription(HonBinarySensorEntityDescriptionMixin, Bi
BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = { BINARY_SENSORS: dict[str, tuple[HonBinarySensorEntityDescription, ...]] = {
"WM": ( "WM": (
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="lastConnEvent.category", key="attributes.lastConnEvent.category",
name="Connection", name="Connection",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
on_value="CONNECTED", on_value="CONNECTED",
), ),
HonBinarySensorEntityDescription( HonBinarySensorEntityDescription(
key="doorLockStatus", key="doorLockStatus",
name="Door Locked", name="Door",
device_class=BinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR,
on_value="0", on_value="0",
), ),
@ -53,9 +53,9 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := BINARY_SENSORS.get(device.appliance_type_name): if descriptions := BINARY_SENSORS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.data.get(description.key): if not device.get(description.key):
_LOGGER.info("Can't setup %s", description.key) _LOGGER.info("Can't setup %s", description.key)
continue continue
appliances.extend([ appliances.extend([
@ -78,9 +78,9 @@ class HonBinarySensorEntity(HonEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
return self._device.data.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.data.get(self.entity_description.key, "") 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

@ -9,16 +9,16 @@ from .hon import HonCoordinator, HonEntity
BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = {
"WM": ( "WM": (
ButtonEntityDescription( # ButtonEntityDescription(
key="pauseProgram", # key="pauseProgram",
name="Pause Program", # name="Pause Program",
icon="mdi:pause", # icon="mdi:pause",
), # ),
ButtonEntityDescription( # ButtonEntityDescription(
key="resumeProgram", # key="resumeProgram",
name="Resume Program", # name="Resume Program",
icon="mdi:play-pause", # icon="mdi:play-pause",
), # ),
), ),
} }
@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := BUTTONS.get(device.appliance_type_name): if descriptions := BUTTONS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.commands.get(description.key): if not device.commands.get(description.key):
continue continue

View File

@ -1,4 +1,4 @@
DOMAIN = "haier" DOMAIN = "hon"
PLATFORMS = [ PLATFORMS = [
"sensor", "sensor",

View File

@ -28,10 +28,10 @@ class HonEntity(CoordinatorEntity):
def device_info(self): def device_info(self):
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device.mac_address)}, identifiers={(DOMAIN, self._device.mac_address)},
manufacturer=self._device.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.fw_version, sw_version=self._device.get("fwVersion", ""),
) )

View File

@ -0,0 +1,12 @@
{
"domain": "hon",
"name": "Haier hOn",
"codeowners": ["@Andre0512"],
"config_flow": true,
"documentation": "https://github.com/Andre0512/hon/",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/Andre0512/hon/issues",
"requirements": ["pyhOn==0.3.3"],
"version": "0.2.0"
}

View File

@ -20,19 +20,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
NumberEntityDescription( NumberEntityDescription(
key="startProgram.delayTime", key="startProgram.delayTime",
name="Delay Time", name="Delay Time",
icon="mdi:timer", 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",
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:timer", icon="mdi:clock-start",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES native_unit_of_measurement=UnitOfTime.MINUTES
), ),
@ -52,7 +53,7 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := NUMBERS.get(device.appliance_type_name): if descriptions := NUMBERS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
appliances.extend([ appliances.extend([
HonNumberEntity(hass, coordinator, entry, device, description)] HonNumberEntity(hass, coordinator, entry, device, description)]
@ -66,6 +67,7 @@ class HonNumberEntity(HonEntity, NumberEntity):
super().__init__(hass, entry, coordinator, device) super().__init__(hass, entry, coordinator, device)
self._coordinator = coordinator self._coordinator = coordinator
self._device = device
self._data = device.settings[description.key] self._data = device.settings[description.key]
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}" self._attr_unique_id = f"{super().unique_id}{description.key}"
@ -77,18 +79,18 @@ class HonNumberEntity(HonEntity, NumberEntity):
@property @property
def native_value(self) -> float | None: def native_value(self) -> float | None:
return self._data.value return self._device.get(self.entity_description.key)
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
self._data.value = value self._device.settings[self.entity_description.key].value = value
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
self._data = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
if isinstance(self._data, HonParameterRange): if isinstance(setting, HonParameterRange):
self._attr_native_max_value = self._data.max self._attr_native_max_value = setting.max
self._attr_native_min_value = self._data.min self._attr_native_min_value = setting.min
self._attr_native_step = self._data.step self._attr_native_step = setting.step
self._attr_native_value = self._data.value self._attr_native_value = setting.value
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -50,9 +50,9 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := SELECTS.get(device.appliance_type_name): if descriptions := SELECTS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.data.get(description.key): if not device.get(description.key):
continue continue
appliances.extend([ appliances.extend([
HonSelectEntity(hass, coordinator, entry, device, description)] HonSelectEntity(hass, coordinator, entry, device, description)]
@ -66,32 +66,31 @@ class HonSelectEntity(HonEntity, SelectEntity):
self._coordinator = coordinator self._coordinator = coordinator
self._device = device self._device = device
self._data = device.settings[description.key]
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}" self._attr_unique_id = f"{super().unique_id}{description.key}"
if not isinstance(self._data, HonParameterFixed): if not isinstance(self._device.settings[description.key], HonParameterFixed):
self._attr_options: list[str] = self._data.values self._attr_options: list[str] = device.settings[description.key].values
else: else:
self._attr_options = [self._data.value] self._attr_options: list[str] = [device.settings[description.key].value]
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
value = self._data.value value = self._device.settings[self.entity_description.key].value
if value is None or value not in self._attr_options: if value is None or value not in self._attr_options:
return None return None
return value return value
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
self._data.value = option self._device.settings[self.entity_description.key].value = option
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
self._data = self._device.settings[self.entity_description.key] setting = self._device.settings[self.entity_description.key]
if not isinstance(self._data, HonParameterFixed): if not isinstance(self._device.settings[self.entity_description.key], HonParameterFixed):
self._attr_options: list[str] = self._data.values self._attr_options: list[str] = setting.values
else: else:
self._attr_options = [self._data.value] self._attr_options = [setting.value]
self._attr_native_value = self._data.value self._attr_native_value = setting.value
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -65,13 +65,13 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = {
), ),
SensorEntityDescription( SensorEntityDescription(
key="machMode", key="machMode",
name="Machine Last Status", name="Machine Status",
icon="mdi:information", icon="mdi:information",
translation_key="mode" translation_key="mode"
), ),
SensorEntityDescription( SensorEntityDescription(
key="errors", key="errors",
name="Last Error", name="Error",
icon="mdi:math-log", icon="mdi:math-log",
translation_key="errors" translation_key="errors"
), ),
@ -105,9 +105,9 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := SENSORS.get(device.appliance_type_name): if descriptions := SENSORS.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if not device.data.get(description.key): if not device.get(description.key):
continue continue
appliances.extend([ appliances.extend([
HonSensorEntity(hass, coordinator, entry, device, description)] HonSensorEntity(hass, coordinator, entry, device, description)]
@ -127,9 +127,9 @@ class HonSensorEntity(HonEntity, SensorEntity):
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
return self._device.data.get(self.entity_description.key, "") return self._device.get(self.entity_description.key, "")
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
self._attr_native_value = self._device.data.get(self.entity_description.key, "") self._attr_native_value = self._device.get(self.entity_description.key, "")
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -27,20 +27,29 @@ class HonSwitchEntityDescription(HonSwitchEntityDescriptionMixin,
SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = { SWITCHES: dict[str, tuple[HonSwitchEntityDescription, ...]] = {
"WM": ( "WM": (
HonSwitchEntityDescription( HonSwitchEntityDescription(
key="startProgram", key="active",
name="Start Program", name="Washing Machine",
icon="mdi:play", icon="mdi:washing-machine",
turn_on_key="startProgram", turn_on_key="startProgram",
turn_off_key="stopProgram", turn_off_key="stopProgram",
), ),
HonSwitchEntityDescription(
key="pause",
name="Pause Washing Machine",
icon="mdi:pause",
turn_on_key="pauseProgram",
turn_off_key="resumeProgram",
),
HonSwitchEntityDescription( HonSwitchEntityDescription(
key="startProgram.delayStatus", key="startProgram.delayStatus",
name="Delay Status", name="Delay Status",
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",
entity_category=EntityCategory.CONFIG entity_category=EntityCategory.CONFIG
), ),
), ),
@ -59,9 +68,9 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> Non
hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator hass.data[DOMAIN]["coordinators"][device.mac_address] = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if descriptions := SWITCHES.get(device.appliance_type_name): if descriptions := SWITCHES.get(device.appliance_type):
for description in descriptions: for description in descriptions:
if device.data.get(description.key) is not None or device.commands.get(description.key) is not None: if device.get(description.key) is not None or device.commands.get(description.key) is not None:
appliances.extend([ appliances.extend([
HonSwitchEntity(hass, coordinator, entry, device, description)] HonSwitchEntity(hass, coordinator, entry, device, description)]
) )
@ -81,7 +90,7 @@ 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
@ -90,7 +99,7 @@ class HonSwitchEntity(HonEntity, SwitchEntity):
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.data.get(self.entity_description.key, "") 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:
if self.entity_category == EntityCategory.CONFIG: if self.entity_category == EntityCategory.CONFIG:

View File

@ -2,7 +2,6 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Haier hOn",
"description": "Please enters your hOn credentials", "description": "Please enters your hOn credentials",
"data": { "data": {
"email": "Email Address", "email": "Email Address",

View File

@ -2,4 +2,4 @@
"name": "Haier hOn", "name": "Haier hOn",
"render_readme": true, "render_readme": true,
"homeassistant": "2023.2.0" "homeassistant": "2023.2.0"
} }