mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Discover new device at runtime in Plugwise (#117688)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
6b7ff2bf44
commit
83e62c5239
@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PlugwiseConfigEntry
|
||||
@ -83,22 +83,32 @@ async def async_setup_entry(
|
||||
"""Set up the Smile binary_sensors from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[PlugwiseBinarySensorEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (binary_sensors := device.get("binary_sensors")):
|
||||
continue
|
||||
for description in BINARY_SENSORS:
|
||||
if description.key not in binary_sensors:
|
||||
continue
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
entities.append(
|
||||
PlugwiseBinarySensorEntity(
|
||||
coordinator,
|
||||
device_id,
|
||||
description,
|
||||
entities: list[PlugwiseBinarySensorEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (binary_sensors := device.get("binary_sensors")):
|
||||
continue
|
||||
for description in BINARY_SENSORS:
|
||||
if description.key not in binary_sensors:
|
||||
continue
|
||||
|
||||
entities.append(
|
||||
PlugwiseBinarySensorEntity(
|
||||
coordinator,
|
||||
device_id,
|
||||
description,
|
||||
)
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.climate import (
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@ -33,11 +33,21 @@ async def async_setup_entry(
|
||||
"""Set up the Smile Thermostats from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseClimateEntity(coordinator, device_id)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
if device["dev_class"] in MASTER_THERMOSTATS
|
||||
)
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseClimateEntity(coordinator, device_id)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
if device["dev_class"] in MASTER_THERMOSTATS
|
||||
)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
|
@ -15,11 +15,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER
|
||||
from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER
|
||||
|
||||
|
||||
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
||||
@ -54,14 +55,13 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
||||
timeout=30,
|
||||
websession=async_get_clientsession(hass, verify_ssl=False),
|
||||
)
|
||||
self.device_list: list[dr.DeviceEntry] = []
|
||||
self.new_devices: bool = False
|
||||
|
||||
async def _connect(self) -> None:
|
||||
"""Connect to the Plugwise Smile."""
|
||||
self._connected = await self.api.connect()
|
||||
self.api.get_all_devices()
|
||||
self.update_interval = DEFAULT_SCAN_INTERVAL.get(
|
||||
str(self.api.smile_type), timedelta(seconds=60)
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> PlugwiseData:
|
||||
"""Fetch data from Plugwise."""
|
||||
@ -81,4 +81,13 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
||||
raise ConfigEntryError("Device with unsupported firmware") from err
|
||||
except ConnectionFailedError as err:
|
||||
raise UpdateFailed("Failed to connect to the Plugwise Smile") from err
|
||||
|
||||
device_reg = dr.async_get(self.hass)
|
||||
device_list = dr.async_entries_for_config_entry(
|
||||
device_reg, self.config_entry.entry_id
|
||||
)
|
||||
|
||||
self.new_devices = len(data.devices.keys()) - len(self.device_list) > 0
|
||||
self.device_list = device_list
|
||||
|
||||
return data
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PlugwiseConfigEntry
|
||||
@ -71,15 +71,24 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Plugwise number platform."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseNumberEntity(coordinator, device_id, description)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
for description in NUMBER_TYPES
|
||||
if description.key in device
|
||||
)
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseNumberEntity(coordinator, device_id, description)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
for description in NUMBER_TYPES
|
||||
if description.key in device
|
||||
)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
||||
|
@ -9,7 +9,7 @@ from plugwise import Smile
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import STATE_ON, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PlugwiseConfigEntry
|
||||
@ -66,12 +66,22 @@ async def async_setup_entry(
|
||||
"""Set up the Smile selector from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseSelectEntity(coordinator, device_id, description)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
for description in SELECT_TYPES
|
||||
if description.options_key in device
|
||||
)
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
PlugwiseSelectEntity(coordinator, device_id, description)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
for description in SELECT_TYPES
|
||||
if description.options_key in device
|
||||
)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
||||
|
@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PlugwiseConfigEntry
|
||||
@ -408,23 +408,33 @@ async def async_setup_entry(
|
||||
"""Set up the Smile sensors from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[PlugwiseSensorEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (sensors := device.get("sensors")):
|
||||
continue
|
||||
for description in SENSORS:
|
||||
if description.key not in sensors:
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
entities: list[PlugwiseSensorEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (sensors := device.get("sensors")):
|
||||
continue
|
||||
for description in SENSORS:
|
||||
if description.key not in sensors:
|
||||
continue
|
||||
|
||||
entities.append(
|
||||
PlugwiseSensorEntity(
|
||||
coordinator,
|
||||
device_id,
|
||||
description,
|
||||
entities.append(
|
||||
PlugwiseSensorEntity(
|
||||
coordinator,
|
||||
device_id,
|
||||
description,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.switch import (
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PlugwiseConfigEntry
|
||||
@ -62,15 +62,28 @@ async def async_setup_entry(
|
||||
"""Set up the Smile switches from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[PlugwiseSwitchEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (switches := device.get("switches")):
|
||||
continue
|
||||
for description in SWITCHES:
|
||||
if description.key not in switches:
|
||||
@callback
|
||||
def _add_entities() -> None:
|
||||
"""Add Entities."""
|
||||
if not coordinator.new_devices:
|
||||
return
|
||||
|
||||
entities: list[PlugwiseSwitchEntity] = []
|
||||
for device_id, device in coordinator.data.devices.items():
|
||||
if not (switches := device.get("switches")):
|
||||
continue
|
||||
entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
|
||||
async_add_entities(entities)
|
||||
for description in SWITCHES:
|
||||
if description.key not in switches:
|
||||
continue
|
||||
entities.append(
|
||||
PlugwiseSwitchEntity(coordinator, device_id, description)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities()
|
||||
|
||||
|
||||
class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Tests for the Plugwise Climate integration."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from plugwise.exceptions import (
|
||||
ConnectionFailedError,
|
||||
@ -15,15 +16,45 @@ from homeassistant.components.plugwise.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
HA_PLUGWISE_SMILE_ASYNC_UPDATE = (
|
||||
"homeassistant.components.plugwise.coordinator.Smile.async_update"
|
||||
)
|
||||
HEATER_ID = "1cbf783bb11e4a7c8a6843dee3a86927" # Opentherm device_id for migration
|
||||
PLUG_ID = "cd0ddb54ef694e11ac18ed1cbce5dbbd" # VCR device_id for migration
|
||||
SECONDARY_ID = (
|
||||
"1cbf783bb11e4a7c8a6843dee3a86927" # Heater_central device_id for migration
|
||||
)
|
||||
TOM = {
|
||||
"01234567890abcdefghijklmnopqrstu": {
|
||||
"available": True,
|
||||
"dev_class": "thermo_sensor",
|
||||
"firmware": "2020-11-04T01:00:00+01:00",
|
||||
"hardware": "1",
|
||||
"location": "f871b8c4d63549319221e294e4f88074",
|
||||
"model": "Tom/Floor",
|
||||
"name": "Tom Badkamer",
|
||||
"sensors": {
|
||||
"battery": 99,
|
||||
"temperature": 18.6,
|
||||
"temperature_difference": 2.3,
|
||||
"valve_position": 0.0,
|
||||
},
|
||||
"temperature_offset": {
|
||||
"lower_bound": -2.0,
|
||||
"resolution": 0.1,
|
||||
"setpoint": 0.1,
|
||||
"upper_bound": 2.0,
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "ABCD012345670A01",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_load_unload_config_entry(
|
||||
@ -165,3 +196,63 @@ async def test_migrate_unique_id_relay(
|
||||
entity_migrated = entity_registry.async_get(entity.entity_id)
|
||||
assert entity_migrated
|
||||
assert entity_migrated.unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_update_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_smile_adam_2: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a clean-up of the device_registry."""
|
||||
utcnow = dt_util.utcnow()
|
||||
data = mock_smile_adam_2.async_update.return_value
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
len(
|
||||
er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
)
|
||||
== 28
|
||||
)
|
||||
assert (
|
||||
len(
|
||||
dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
)
|
||||
== 6
|
||||
)
|
||||
|
||||
# Add a 2nd Tom/Floor
|
||||
data.devices.update(TOM)
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
async_fire_time_changed(hass, utcnow + timedelta(minutes=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
len(
|
||||
er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
)
|
||||
== 33
|
||||
)
|
||||
assert (
|
||||
len(
|
||||
dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
)
|
||||
== 7
|
||||
)
|
||||
item_list: list[str] = []
|
||||
for device_entry in list(device_registry.devices.values()):
|
||||
item_list.extend(x[1] for x in device_entry.identifiers)
|
||||
assert "01234567890abcdefghijklmnopqrstu" in item_list
|
||||
|
Loading…
x
Reference in New Issue
Block a user