Skip to content

Commit

Permalink
Add name update support (#11)
Browse files Browse the repository at this point in the history
When a device name is updated, this update will be propagated to Elro Connects app.
Note: The name length is limited to 15 characters.
  • Loading branch information
jbouwh authored Aug 9, 2022
1 parent 79d03c6 commit e0c363a
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 17 deletions.
2 changes: 1 addition & 1 deletion custom_components/elro_connects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async def _async_update_data() -> dict[int, dict]:
for device_id, state_base in coordinator_update.items():
state_base[ATTR_DEVICE_STATE] = STATE_UNKNOWN
try:
await hass.async_create_task(elro_connects_api.async_update())
await elro_connects_api.async_update()
device_update = copy.deepcopy(elro_connects_api.data)
for device_id, device_data in device_update.items():
if ATTR_DEVICE_STATE not in device_data:
Expand Down
87 changes: 77 additions & 10 deletions custom_components/elro_connects/device.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"""Elro Connects K1 device communication."""
from __future__ import annotations

import asyncio
import logging
from typing import Any

from elro.api import K1
from elro.command import GET_ALL_EQUIPMENT_STATUS, GET_DEVICE_NAMES
from elro.command import (
GET_ALL_EQUIPMENT_STATUS,
GET_DEVICE_NAMES,
SET_DEVICE_NAME,
CommandAttributes,
)
from elro.device import (
ALARM_CO,
ALARM_FIRE,
Expand All @@ -17,8 +24,9 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
Expand Down Expand Up @@ -50,39 +58,97 @@ def __init__(
) -> None:
"""Initialize the K1 connector."""
self._coordinator = coordinator
self.hass = coordinator.hass
self._entry = entry

self._data: dict[int, dict] = {}
self._api_lock = asyncio.Lock()
self._connector_id = entry.data[CONF_CONNECTOR_ID]
self._retry_count = 0

self._device_registry_updated = coordinator.hass.bus.async_listen(
EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_updated
)

K1.__init__(
self,
entry.data[CONF_HOST],
entry.data[CONF_CONNECTOR_ID],
entry.data[CONF_PORT],
entry.data.get(CONF_API_KEY),
)

async def _async_device_updated(self, event: Event) -> None:
"""Propagate name changes though the connector."""
if (
event.data["action"] != "update"
or "name_by_user" not in event.data["changes"]
):
# Ignore "create" action and other changes
return

device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get(event.data["device_id"])
device_unique_id: str = device_entry.identifiers.copy().pop()[1]
device_id_str = device_unique_id[len(self.connector_id) + 1 :]
if self._entry.entry_id not in device_entry.config_entries or not device_id_str:
# Not a valid device name or not a related entry
return
device_id = int(device_id_str)
if device_id not in self.data:
# the device is not in the coordinator data hence we cannot update it
return False

if device_entry.name != device_entry.name_by_user:
await self.async_command(
SET_DEVICE_NAME,
device_ID=device_id,
device_name=device_entry.name_by_user[:15]
if len(device_entry.name_by_user) > 15
else device_entry.name_by_user,
)

async def async_update(self) -> None:
"""Synchronize with the K1 connector."""
new_data: dict[int, dict] = {}
try:

async def _async_update() -> None:
new_data: dict[int, dict] = {}
update_status = await self.async_process_command(GET_ALL_EQUIPMENT_STATUS)
new_data = update_status
update_names = await self.async_process_command(GET_DEVICE_NAMES)
update_state_data(new_data, update_names)
self._retry_count = 0
self._data = new_data

try:
async with self._api_lock:
await self.hass.async_add_job(_async_update())
except K1.K1ConnectionError as err:
self._retry_count += 1
if not self._data or self._retry_count >= MAX_RETRIES:
raise K1.K1ConnectionError(err) from err

async def async_command(
self,
command: CommandAttributes,
**argv: int | str,
) -> dict[int, dict[str, Any]] | None:
"""Execute a synchronized command through the K1 connector."""
async with self._api_lock:
return self.hass.async_add_job(self.async_process_command(command, **argv))

async def async_update_settings(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Process updated settings."""
await self.async_configure(
entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data.get(CONF_API_KEY)
)
async with self._api_lock:
hass.async_create_task(
self.async_configure(
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data.get(CONF_API_KEY),
)
)

@property
def data(self) -> dict[int, dict]:
Expand Down Expand Up @@ -143,12 +209,12 @@ def device_info(self) -> DeviceInfo:
"""Return info for device registry."""
# connector
device_registry = dr.async_get(self.hass)
k1_device = device_registry.async_get_or_create(
device_registry.async_get_or_create(
model="K1 (SF40GA)",
config_entry_id=self._entry.entry_id,
identifiers={(DOMAIN, self._connector_id)},
manufacturer="Elro",
name="Elro Connects K1 connector",
name=f"Elro Connects K1 {self._connector_id}",
)
# sub device
device_type = self.data[ATTR_DEVICE_TYPE]
Expand All @@ -159,6 +225,7 @@ def device_info(self) -> DeviceInfo:
if device_type in DEVICE_MODELS
else device_type,
name=self.name,
via_device=(DOMAIN, k1_device.id),
# Link to K1 connector
via_device=(DOMAIN, self._connector_id),
)
return device_info
2 changes: 1 addition & 1 deletion custom_components/elro_connects/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"requirements": ["lib-elro-connects==0.5.3"],
"codeowners": ["@jbouwh"],
"iot_class": "local_polling",
"version": "0.1.8"
"version": "0.1.9"
}
6 changes: 2 additions & 4 deletions custom_components/elro_connects/siren.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ def is_on(self) -> bool | None:
async def async_turn_on(self, **kwargs) -> None:
"""Send a test alarm request."""
_LOGGER.debug("Sending test alarm request for entity %s", self.entity_id)
await self._elro_connects_api.async_connect()
await self._elro_connects_api.async_process_command(
await self._elro_connects_api.async_command(
self._description.test_alarm, device_ID=self._device_id
)

Expand All @@ -152,8 +151,7 @@ async def async_turn_on(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs) -> None:
"""Send a silence alarm request."""
_LOGGER.debug("Sending silence alarm request for entity %s", self.entity_id)
await self._elro_connects_api.async_connect()
await self._elro_connects_api.async_process_command(
await self._elro_connects_api.async_command(
self._description.silence_alarm, device_ID=self._device_id
)

Expand Down
67 changes: 66 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ async def test_remove_device_from_config_entry(
mock_k1_connector["result"].return_value = updated_status_data
time = dt.now() + timedelta(seconds=30)
async_fire_time_changed(hass, time)
# await coordinator.async_request_refresh()
# Wait for the update to be processed
await hass.async_block_till_done()
await hass.async_block_till_done()

Expand All @@ -178,3 +178,68 @@ async def test_remove_device_from_config_entry(
)
assert device_entry
assert not await async_remove_config_entry_device(hass, mock_entry, device_entry)


async def test_update_device_name(
hass: HomeAssistant,
mock_k1_connector: dict[AsyncMock],
mock_entry: ConfigEntry,
) -> None:
"""Test updating the name of the device through the K1 connector."""

# Initial status holds device info for device [1,2,4]
initial_status_data = copy.deepcopy(MOCK_DEVICE_STATUS_DATA)
mock_k1_connector["result"].return_value = initial_status_data
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()

connector_id = hass.data[DOMAIN][mock_entry.entry_id].connector_id
device_registry = dr.async_get(hass)
# Test updating the device name for siren 4 will work
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, f"{connector_id}_4")}
)
assert device_entry

# update the name
mock_k1_connector["result"].reset_mock()
device_registry.async_update_device(
device_id=device_entry.id, name_by_user="Some long new name"
)
await hass.async_block_till_done()

# Check the new name was set (max length 15)
assert mock_k1_connector["result"].call_count == 1
assert mock_k1_connector["result"].mock_calls[0][2] == {
"device_ID": 4,
"device_name": "Some long new n",
}

# Check the name was not set if the device is not in the coordinator
updated_status_data = copy.deepcopy(MOCK_DEVICE_STATUS_DATA)
updated_status_data.pop(4)
mock_k1_connector["result"].return_value = updated_status_data
time = dt.now() + timedelta(seconds=30)
async_fire_time_changed(hass, time)
# Wait for the update to be processed
await hass.async_block_till_done()
await hass.async_block_till_done()
mock_k1_connector["result"].reset_mock()
device_registry.async_update_device(
device_id=device_entry.id, name_by_user="Name update for non existent device"
)
await hass.async_block_till_done()
assert mock_k1_connector["result"].call_count == 0

# update the K1 connector name
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, f"{connector_id}")}
)
assert device_entry
mock_k1_connector["result"].reset_mock()
device_registry.async_update_device(
device_id=device_entry.id, name_by_user="Some long new name"
)
await hass.async_block_till_done()

assert mock_k1_connector["result"].call_count == 0

0 comments on commit e0c363a

Please sign in to comment.