Skip to content

Commit

Permalink
Merge pull request #30 from golles/refactor_component
Browse files Browse the repository at this point in the history
Refactor component
  • Loading branch information
golles authored Nov 9, 2022
2 parents ad81a15 + e9945e3 commit a51064e
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 491 deletions.
50 changes: 30 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,39 @@
![Project Maintenance][maintenance-shield]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]

[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]

Kamstrup 403 custom component for Home Assistant.

<img width="663" alt="info" src="https://user-images.githubusercontent.com/2211503/173236049-10647d83-9be6-49a6-a90b-671a8860c743.png">
<img width="660" alt="info" src="https://user-images.githubusercontent.com/2211503/200671065-201f84bc-0d01-4a87-8fd9-3da3beedfb5d.png">

## Requirements

To use this component, you'll need a cable with an IR read/write head and connect your machine running Home Assistant directly to the IR sensor of the Kamstrup meter.
The one from [Volkszaehler.org](https://wiki.volkszaehler.org/hardware/controllers/ir-schreib-lesekopf) seems to work fine, but might be hard to get.
The read/write head looks like this:
To use this custom component, you'll need a cable with an IR read/write head and connect your machine running Home Assistant directly to the IR sensor of the Kamstrup meter.
The read/write head looks like this:<br>
![cable](https://user-images.githubusercontent.com/2211503/136630069-9da49f09-6f9c-4618-8255-40195405f21a.jpg)

### Placing the IR head

There is not a lot of tolerance for placing the IR head on the meter, it can be very tedious to get this right. The best way is to fix the head to the meter. I suggest this 3D-printed holder from [Thingiverse](https://www.thingiverse.com/thing:5615493).<br>
![647d4ce9-4e72-4c54-95e6-d4caf720a79b](https://user-images.githubusercontent.com/2211503/200637881-19fd9166-ea5c-4805-a127-4b9be87f2de5.jpeg)

### Supported devices

This component is created to only support the Kamstrup 403 meter. This is a conscious decision because I do own this device and I can only offer support for that. There are some similar devices that work with the same communication protocol. If it does work for a meter that isn't listed below, please create a [feature request](https://github.com/golles/ha-kamstrup_403/issues/new?template=supported_device.yaml) so I can update the table.
Meter | Supported | Description
-- | -- | --
Kamstrup 403 | Yes |
Kamstrup 402 | Yes | Confirmed in [#14](https://github.com/golles/ha-kamstrup_403/issues/27)
Kamstrup 601 | Yes | Confirmed in [#14](https://github.com/golles/ha-kamstrup_403/issues/14)
Kamstrup 602 | Yes | Confirmed in [#10](https://github.com/golles/ha-kamstrup_403/issues/10)
Kamstrup 603 | Yes | Confirmed in [#18](https://github.com/golles/ha-kamstrup_403/issues/18)
Kamstrup 402 | Yes | Confirmed in [#14](https://github.com/golles/ha-kamstrup_403/issues/27)
Kamstrup MC66C | No | Supported in my [old component](https://github.com/golles/Home-Assistant-Sensor-MC66C)


## Installation

### HACS

This component can be installed in your Home Assistant with HACS.
This component can easily be installed in your Home Assistant using HACS.


### Manual
Expand All @@ -53,46 +53,56 @@ This component can be installed in your Home Assistant with HACS.
6. Restart Home Assistant
7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Kamstrup 403"

Using your HA configuration directory (folder) as a starting point you should now also have this:
Using your HA configuration directory (folder) as a starting point you should now also have these files:

```text
custom_components/kamstrup_403/translations/en.json
custom_components/kamstrup_403/translations/nl.json
custom_components/kamstrup_403/__init__.py
custom_components/kamstrup_403/config_flow.py
custom_components/kamstrup_403/const.py
custom_components/kamstrup_403/entity.py
custom_components/kamstrup_403/kamstrup.py
custom_components/kamstrup_403/manifest.json
custom_components/kamstrup_403/sensor.py
```

## Configuration is done in the UI

It's recommended to use devices as `/dev/serial/by-id` and not `/dev/ttyUSB1` as the port. This because the first example is a stable identifier, while the second one can change when USB devices are added or removed, or even when you perform a system reboot.
The port will be like: `/dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_D307PBVY-if00-port0`.
It's recommended to use devices as `/dev/serial/by-id` and not `/dev/ttyUSB1` as the port. This is because the first example is a stable identifier, while the second can change when USB devices are added or removed, or even when you perform a system reboot.<br>
The port should look like this: `/dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_D307PBVY-if00-port0`.

Some meters contain a battery, and communicating with the meter does impact battery life. By default, this component updates every 3600 seconds (1 hour). From version `1.2.0`, you can configure the update interval. You can do this by pressing `configure` on the Integrations page:

Some meters contain a battery, and communicating with the meter does impact battery life. By default, this component updates every 60 seconds. From version `1.2.0`, you can configure the update interval on the Integrations page:
<img width="290" alt="integration" src="https://user-images.githubusercontent.com/2211503/200671075-39c7a812-42a2-4a4d-8934-6ea37517a400.png"> <img width="392" alt="configure" src="https://user-images.githubusercontent.com/2211503/200671074-7b4c73da-f4cf-47bb-b293-46e5d8850163.png">

<img width="290" alt="opt1" src="https://user-images.githubusercontent.com/2211503/173235828-fd130b51-99b0-4522-b697-4d69df51925d.png"> <img width="392" alt="opt2" src="https://user-images.githubusercontent.com/2211503/173235826-ffd79769-cc2c-4404-9b79-d233aef8587e.png">
## Integration in the energy dashboard

The `Heat Energy (E1)` sensor can be added to the energy dashboard as an individual device.<br>
There is also a sensor named `Heat Energy to Gas`, this sensor is disabled by default and can be enabled manually. This is a conversion sensor, that takes the `Heat Energy (E1)` value, and represents itself as a `gas` sensor. This can be added to the energy dashboard under the gas section.

## Collect logs

When you want to report an issue, please add logs from this component. You can enable logging for this component by configuring the logger in Home Assistant as follows:
```yaml
logger:
default: warn
logs:
custom_components.kamstrup_403: debug
```
More info can be found on the [Home Assistant logger integration page](https://www.home-assistant.io/integrations/logger)
## Contributions are welcome!
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
***
[knmi]: https://github.com/golles/ha-kamstrup_403
[buymecoffee]: https://www.buymeacoffee.com/golles
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
[commits-shield]: https://img.shields.io/github/commit-activity/y/golles/ha-kamstrup_403.svg?style=for-the-badge
[commits]: https://github.com/golles/ha-kamstrup_403/commits/main
[hacs]: https://github.com/custom-components/hacs
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
[discord]: https://discord.gg/Qa5fW2R
[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
[forum]: https://community.home-assistant.io/
[license-shield]: https://img.shields.io/github/license/golles/ha-kamstrup_403.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-golles-blue.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/golles/ha-kamstrup_403.svg?style=for-the-badge
Expand Down
133 changes: 84 additions & 49 deletions custom_components/kamstrup_403/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
"""
import asyncio
from datetime import timedelta
from typing import Any, List
import logging
import serial

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .kamstrup import Kamstrup
Expand All @@ -23,21 +25,20 @@
DEFAULT_SCAN_INTERVAL,
DEFAULT_TIMEOUT,
DOMAIN,
NAME,
PLATFORMS,
SENSORS,
VERSION,
)

_LOGGER: logging.Logger = logging.getLogger(__package__)


async def async_setup(
hass: HomeAssistant, config: Config
): # pylint: disable=unused-argument
async def async_setup(_hass: HomeAssistant, _config: Config) -> bool:
"""Set up this integration using YAML is not supported."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})
Expand All @@ -46,83 +47,117 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
scan_interval_seconds = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
scan_interval = timedelta(seconds=scan_interval_seconds)

_LOGGER.debug("Set up entry, with scan_interval %s seconds", scan_interval_seconds)

client = Kamstrup(port, DEFAULT_BAUDRATE, DEFAULT_TIMEOUT)

coordinator = KamstrupUpdateCoordinator(
hass, client=client, scan_interval=scan_interval
device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, port)},
manufacturer=NAME,
name=NAME,
model=VERSION,
)
await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady
coordinator = KamstrupUpdateCoordinator(
hass=hass, client=client, scan_interval=scan_interval, device_info=device_info
)

hass.data[DOMAIN][entry.entry_id] = coordinator

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
await hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

entry.async_on_unload(entry.add_update_listener(async_reload_entry))
await coordinator.async_config_entry_first_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload this config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
return unload_ok


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)


class KamstrupUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the Kamstrup serial reader."""

def __init__(
self, hass: HomeAssistant, client: Kamstrup, scan_interval: int
self,
hass: HomeAssistant,
client: Kamstrup,
scan_interval: int,
device_info: DeviceInfo,
) -> None:
"""Initialize."""
self.kamstrup = client
self.platforms = []
self.device_info = device_info

self._commands: List[str] = []

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=scan_interval)

async def _async_update_data(self):
def register_command(self, command: str) -> None:
"""Add a command to the commands list."""
_LOGGER.debug("Register command %s", command)
self._commands.append(command)

def unregister_command(self, command: str) -> None:
"""Remove a command from the commands list."""
_LOGGER.debug("Unregister command %s", command)
self._commands.remove(command)

async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
_LOGGER.debug("KamstrupUpdateCoordinator: _async_update_data start")
_LOGGER.debug("Start update")

data = {}
for key, sensor in SENSORS.items():
failed_counter = 0

for command in self._commands:
try:
value, unit = self.kamstrup.readvar(sensor["command"])
data[sensor["command"]] = {"value": value, "unit": unit}
_LOGGER.debug("New value for sensor %s, value: %s %s", sensor["name"], value, unit)
value, unit = self.kamstrup.readvar(int(command))
data[command] = {"value": value, "unit": unit}
_LOGGER.debug(
"New value for sensor %s, value: %s %s", command, value, unit
)

if value is None and unit is None:
failed_counter += 1

await asyncio.sleep(1)
except (serial.SerialException) as exception:
_LOGGER.error(
"Device disconnected or multiple access on port? \nException: %e",
exception,
)
except (Exception) as exception: # pylint: disable=broad-except
_LOGGER.error(
"Error reading %s \nException: %s", sensor["name"], exception
)
return data


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
if platform in coordinator.platforms
]
)
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)

return unloaded
except (Exception) as exception:
_LOGGER.error("Error reading %s \nException: %s", command, exception)
raise UpdateFailed() from exception

if failed_counter == len(data):
_LOGGER.error(
"Finished update, No readings from the meter. Please check the IR connection"
)
else:
_LOGGER.debug(
"Finished update, %s/%s readings failed", failed_counter, len(data)
)

async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
return data
Loading

0 comments on commit a51064e

Please sign in to comment.