Skip to content

Commit

Permalink
Merge pull request #15 from mampfes/json_replace_parse
Browse files Browse the repository at this point in the history
refactor: use JSON live export
  • Loading branch information
mampfes authored Dec 28, 2024
2 parents 30d112a + 1685376 commit 4d76b76
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 266 deletions.
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ This component adds support for [Bayernlüfter](https://www.bayernluft.de) devic

If you like this component, please give it a star on [github](https://github.com/mampfes/ha_bayernluefter).

This integration requires at least revision firmware revision WS32234601 (which supports JSON export).

## Installation

1. Ensure that [HACS](https://hacs.xyz) is installed.
Expand All @@ -24,15 +26,6 @@ In case you would like to install manually:

[![](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=bayernluefter)

## Template File

To get all available values from your Bayernlüfter device, you have to upload a template file with all available variables:

1. Download the following [template](./doc/export.txt).
2. Open the _Experten-Browser_ of your device: `http://<ip-address/browser.html`
3. Select the downloaded `export.txt` in the _Experten-Browser_ and click on _Hochladen_.
4. Reload the integration in Home Assistant or restart Home Assistant.

## Device Name

By factory default, the device name is equal to the MAC address of the device. To set your own device name:
Expand All @@ -41,6 +34,10 @@ By factory default, the device name is equal to the MAC address of the device. T
2. In the box _Modulkonfiguration_ change the field `DeviceName`.
3. Click on _Speichern und neu starten_.

## Notes

Since firmware version WS32240427, the speed of the 3 fan motors can be controlled individually. But these controls will only work if the device is switched off!!! This is a limitation of the firmware of the device, not the integration.

## Acknowledgements

This component was inspired by <https://github.com/nielstron/ha_bayernluefter>.
54 changes: 45 additions & 9 deletions custom_components/bayernluefter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
from aiohttp import ClientError
from requests.exceptions import RequestException

from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import CoordinatorEntity
Expand Down Expand Up @@ -83,6 +84,43 @@ async def on_update_options_listener(hass: HomeAssistant, entry: ConfigEntry):
coordinator.update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL])


# Migrate entity unique-ids from version 1 to version 2
ENTITY_ID_MAP = {
"_SystemOn": "SystemOn",
"_MaxMode": "TimerActiv",
"_Frozen": "SpeedFrozen",
"_QuerlueftungAktiv": "QuerlueftungAktiv",
"_VermieterMode": "VermieterMode",
"_AbtauMode": "AbtauMode",
"_FrostschutzAktiv": "FrostschutzAktiv",
}


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""

if config_entry.version > 1:
# This means the user has downgraded from a future version
return False

if config_entry.version == 1:

@callback
def update_unique_id(
entity_entry: RegistryEntry,
) -> dict[str, Any] | None:
"""Update unique ID of entity entry."""
(mac, key) = entity_entry.unique_id.split("-", 1)
if key in ENTITY_ID_MAP:
return {"new_unique_id": "-".join([mac, ENTITY_ID_MAP[key]])}
return None

await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)

hass.config_entries.async_update_entry(config_entry, version=2, minor_version=0)
return True


class BayernluefterDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data from Bayernluefter device."""

Expand Down Expand Up @@ -129,18 +167,16 @@ def __init__(
device = coordinator._device
self._coordinator = coordinator
self._device = device
self._attr_unique_id = f"{format_mac(device.raw()['MAC'])}-{description.key}"
self._attr_unique_id = f"{format_mac(device.data['MAC'])}-{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{device.raw()['LocalIP']}",
identifiers={(DOMAIN, format_mac(device.raw()["MAC"]))},
name=device.raw()["DeviceName"],
configuration_url=f"http://{device.data.get('LocalIP')}",
identifiers={(DOMAIN, format_mac(device.data["MAC"]))},
name=device.data.get("DeviceName"),
manufacturer="BAVARIAVENT UG (haftungsbeschränkt) & Co. KG",
model="Bayernlüfter",
sw_version=f"{device.raw()['FW_MainController']} / {device.raw()['FW_WiFi']}", # noqa: E501
sw_version=f"{device.data.get('FW_MainController')} / {device.data.get('FW_WiFi')}", # noqa: E501
)

@property
def available(self) -> bool:
# Note: we only check raw() and not raw_converted() because both have the
# same set of keys
return super().available and self.entity_description.key in self._device.raw()
return super().available and self.entity_description.key in self._device.data
14 changes: 7 additions & 7 deletions custom_components/bayernluefter/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@

SENSOR_TYPES_CONVERTED: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="_FrostschutzAktiv",
key="FrostschutzAktiv",
name="FrostschutzAktiv",
),
BinarySensorEntityDescription(
key="_AbtauMode",
key="AbtauMode",
name="AbtauMode",
),
BinarySensorEntityDescription(
key="_VermieterMode",
key="VermieterMode",
name="VermieterMode",
entity_category=EntityCategory.DIAGNOSTIC,
),
BinarySensorEntityDescription(
key="_QuerlueftungAktiv",
key="QuerlueftungAktiv",
name="QuerlueftungAktiv",
),
BinarySensorEntityDescription(
key="_MaxMode",
key="TimerActiv",
name="TimerAktiv",
),
BinarySensorEntityDescription(
key="_Frozen",
key="SpeedFrozen",
name="SpeedFrozen",
),
)
Expand Down Expand Up @@ -78,4 +78,4 @@ def __init__(
@property
def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self._device.raw_converted()[self.entity_description.key]
return self._device.data[self.entity_description.key]
7 changes: 4 additions & 3 deletions custom_components/bayernluefter/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
class ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Component config flow."""

VERSION = 1
VERSION = 2
MINOR_VERSION = 0

@staticmethod
@callback
Expand Down Expand Up @@ -89,11 +90,11 @@ async def async_step_user(self, user_input=None):
except ValueError:
errors["base"] = "cannot_connect"
else:
user_input[CONF_MAC] = format_mac(device.raw()["MAC"])
user_input[CONF_MAC] = format_mac(device.data["MAC"])
await self.async_set_unique_id(user_input[CONF_MAC])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{device.raw_converted()['DeviceName']} @ {user_input[CONF_HOST]}", # noqa: E501
title=f"{device.data['DeviceName']} @ {user_input[CONF_HOST]}", # noqa: E501
data=user_input,
)

Expand Down
12 changes: 6 additions & 6 deletions custom_components/bayernluefter/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class BayernluefterFan(BayernluefterEntity, FanEntity):
"""A fan implementation for Bayernluefter devices."""

entity_description = FanEntityDescription(
key="_SystemOn",
key="SystemOn",
name="Fan",
)

Expand All @@ -73,27 +73,27 @@ def __init__(
"""Initialize a fan entity for a Bayernluefter device."""
super().__init__(coordinator, self.entity_description)
self._attr_preset_modes = [FanMode.Timer]
if self._device.raw_converted()["SystemMode"] in SYSTEM_MODES_WITH_AUTO:
if self._device.data.get("SystemMode") in SYSTEM_MODES_WITH_AUTO:
self._attr_preset_modes.append(FanMode.Auto)

@property
def is_on(self) -> bool:
"""Return the fan on status."""
return self._device.raw_converted()["_SystemOn"]
return self._device.data.get("SystemOn", False)

@property
def percentage(self) -> int:
"""Return the speed of the fan-"""
return ranged_value_to_percentage(
FAN_SPEED_RANGE, self._device.raw_converted()["Speed_Out"]
FAN_SPEED_RANGE, self._device.data.get("Speed_Out", 0)
)

@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
if self._device.raw_converted()["_MaxMode"]:
if self._device.data["TimerActiv"]:
pm = FanMode.Timer
elif self._device.raw_converted()["_Frozen"]:
elif self._device.data["SpeedFrozen"]:
pm = None
else:
pm = FanMode.Auto
Expand Down
4 changes: 2 additions & 2 deletions custom_components/bayernluefter/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/mampfes/ha_bayernluefter/issues",
"requirements": ["aiohttp", "parse"],
"version": "1.3.1"
"requirements": ["aiohttp"],
"version": "2.0.0"
}
10 changes: 9 additions & 1 deletion custom_components/bayernluefter/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,18 @@ def __init__(
super().__init__(coordinator, description)
self.entity_description = description

@property
def available(self) -> bool:
return (
super().available
and self.entity_description.key in self._device.data
and not self._device.data.get("SystemOn", False)
)

@property
def native_value(self) -> float | None:
"""Return the value reported by the sensor."""
return self._device.raw_converted()[self.entity_description.key]
return self._device.data[self.entity_description.key]

async def async_set_native_value(self, value: float) -> None:
"""Update the native value."""
Expand Down
Loading

0 comments on commit 4d76b76

Please sign in to comment.