mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
2023.9.2 (#100223)
This commit is contained in:
commit
c6ed235010
@ -5,25 +5,35 @@ import logging
|
|||||||
|
|
||||||
from airthings_ble import AirthingsDevice
|
from airthings_ble import AirthingsDevice
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
LIGHT_LUX,
|
LIGHT_LUX,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
|
Platform,
|
||||||
UnitOfPressure,
|
UnitOfPressure,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
from homeassistant.helpers.device_registry import (
|
||||||
|
CONNECTION_BLUETOOTH,
|
||||||
|
DeviceInfo,
|
||||||
|
async_get as device_async_get,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.entity_registry import (
|
||||||
|
RegistryEntry,
|
||||||
|
async_entries_for_device,
|
||||||
|
async_get as entity_async_get,
|
||||||
|
)
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
@ -107,9 +117,43 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
|
||||||
|
"""Migrate entities to new unique ids (with BLE Address)."""
|
||||||
|
ent_reg = entity_async_get(hass)
|
||||||
|
unique_id_trailer = f"_{sensor_name}"
|
||||||
|
new_unique_id = f"{address}{unique_id_trailer}"
|
||||||
|
if ent_reg.async_get_entity_id(DOMAIN, Platform.SENSOR, new_unique_id):
|
||||||
|
# New unique id already exists
|
||||||
|
return
|
||||||
|
dev_reg = device_async_get(hass)
|
||||||
|
if not (
|
||||||
|
device := dev_reg.async_get_device(
|
||||||
|
connections={(CONNECTION_BLUETOOTH, address)}
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
entities = async_entries_for_device(
|
||||||
|
ent_reg,
|
||||||
|
device_id=device.id,
|
||||||
|
include_disabled_entities=True,
|
||||||
|
)
|
||||||
|
matching_reg_entry: RegistryEntry | None = None
|
||||||
|
for entry in entities:
|
||||||
|
if entry.unique_id.endswith(unique_id_trailer) and (
|
||||||
|
not matching_reg_entry or "(" not in entry.unique_id
|
||||||
|
):
|
||||||
|
matching_reg_entry = entry
|
||||||
|
if not matching_reg_entry:
|
||||||
|
return
|
||||||
|
entity_id = matching_reg_entry.entity_id
|
||||||
|
ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)
|
||||||
|
_LOGGER.debug("Migrated entity '%s' to unique id '%s'", entity_id, new_unique_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: config_entries.ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Airthings BLE sensors."""
|
"""Set up the Airthings BLE sensors."""
|
||||||
@ -137,6 +181,7 @@ async def async_setup_entry(
|
|||||||
sensor_value,
|
sensor_value,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
async_migrate(hass, coordinator.data.address, sensor_type)
|
||||||
entities.append(
|
entities.append(
|
||||||
AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
|
AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
|
||||||
)
|
)
|
||||||
@ -165,7 +210,7 @@ class AirthingsSensor(
|
|||||||
if identifier := airthings_device.identifier:
|
if identifier := airthings_device.identifier:
|
||||||
name += f" ({identifier})"
|
name += f" ({identifier})"
|
||||||
|
|
||||||
self._attr_unique_id = f"{name}_{entity_description.key}"
|
self._attr_unique_id = f"{airthings_device.address}_{entity_description.key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={
|
connections={
|
||||||
(
|
(
|
||||||
|
@ -14,10 +14,10 @@
|
|||||||
],
|
],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"bleak==0.21.0",
|
"bleak==0.21.1",
|
||||||
"bleak-retry-connector==3.1.3",
|
"bleak-retry-connector==3.1.3",
|
||||||
"bluetooth-adapters==0.16.1",
|
"bluetooth-adapters==0.16.1",
|
||||||
"bluetooth-auto-recovery==1.2.2",
|
"bluetooth-auto-recovery==1.2.3",
|
||||||
"bluetooth-data-tools==1.11.0",
|
"bluetooth-data-tools==1.11.0",
|
||||||
"dbus-fast==1.95.2"
|
"dbus-fast==1.95.2"
|
||||||
]
|
]
|
||||||
|
@ -47,7 +47,6 @@ from .const import (
|
|||||||
CONF_FILTER,
|
CONF_FILTER,
|
||||||
CONF_GOOGLE_ACTIONS,
|
CONF_GOOGLE_ACTIONS,
|
||||||
CONF_RELAYER_SERVER,
|
CONF_RELAYER_SERVER,
|
||||||
CONF_REMOTE_SNI_SERVER,
|
|
||||||
CONF_REMOTESTATE_SERVER,
|
CONF_REMOTESTATE_SERVER,
|
||||||
CONF_SERVICEHANDLERS_SERVER,
|
CONF_SERVICEHANDLERS_SERVER,
|
||||||
CONF_THINGTALK_SERVER,
|
CONF_THINGTALK_SERVER,
|
||||||
@ -115,7 +114,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_ALEXA_SERVER): str,
|
vol.Optional(CONF_ALEXA_SERVER): str,
|
||||||
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
|
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
|
||||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||||
vol.Optional(CONF_REMOTE_SNI_SERVER): str,
|
|
||||||
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
||||||
vol.Optional(CONF_THINGTALK_SERVER): str,
|
vol.Optional(CONF_THINGTALK_SERVER): str,
|
||||||
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
||||||
|
@ -55,7 +55,6 @@ CONF_ACME_SERVER = "acme_server"
|
|||||||
CONF_ALEXA_SERVER = "alexa_server"
|
CONF_ALEXA_SERVER = "alexa_server"
|
||||||
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
|
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
|
||||||
CONF_RELAYER_SERVER = "relayer_server"
|
CONF_RELAYER_SERVER = "relayer_server"
|
||||||
CONF_REMOTE_SNI_SERVER = "remote_sni_server"
|
|
||||||
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
||||||
CONF_THINGTALK_SERVER = "thingtalk_server"
|
CONF_THINGTALK_SERVER = "thingtalk_server"
|
||||||
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["hass_nabucasa"],
|
"loggers": ["hass_nabucasa"],
|
||||||
"requirements": ["hass-nabucasa==0.70.0"]
|
"requirements": ["hass-nabucasa==0.71.0"]
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ class ComelitSerialBridge(DataUpdateCoordinator):
|
|||||||
raise ConfigEntryAuthFailed
|
raise ConfigEntryAuthFailed
|
||||||
|
|
||||||
devices_data = await self.api.get_all_devices()
|
devices_data = await self.api.get_all_devices()
|
||||||
alarm_data = await self.api.get_alarm_config()
|
|
||||||
await self.api.logout()
|
await self.api.logout()
|
||||||
|
|
||||||
return devices_data | alarm_data
|
return devices_data
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HOP_SCAN_INTERVAL = timedelta(hours=2)
|
HOP_SCAN_INTERVAL = timedelta(minutes=20)
|
||||||
|
|
||||||
|
|
||||||
class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"flow_title": "{serial} ({host})",
|
"flow_title": "{serial} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "For firmware version 7.0 and later, enter the Enphase cloud credentials, for older models models, enter username `installer` without a password.",
|
"description": "For firmware version 7.0 and later, enter the Enphase cloud credentials, for older models, enter username `installer` without a password.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
@ -1096,7 +1096,7 @@ class FritzBoxBaseEntity:
|
|||||||
class FritzRequireKeysMixin:
|
class FritzRequireKeysMixin:
|
||||||
"""Fritz entity description mix in."""
|
"""Fritz entity description mix in."""
|
||||||
|
|
||||||
value_fn: Callable[[FritzStatus, Any], Any]
|
value_fn: Callable[[FritzStatus, Any], Any] | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -1118,8 +1118,11 @@ class FritzBoxBaseCoordinatorEntity(update_coordinator.CoordinatorEntity[AvmWrap
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init device info class."""
|
"""Init device info class."""
|
||||||
super().__init__(avm_wrapper)
|
super().__init__(avm_wrapper)
|
||||||
|
if description.value_fn is not None:
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
avm_wrapper.register_entity_updates(description.key, description.value_fn)
|
avm_wrapper.register_entity_updates(
|
||||||
|
description.key, description.value_fn
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._device_name = device_name
|
self._device_name = device_name
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
"""Support for AVM FRITZ!Box update platform."""
|
"""Support for AVM FRITZ!Box update platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
|
from homeassistant.components.update import (
|
||||||
|
UpdateEntity,
|
||||||
|
UpdateEntityDescription,
|
||||||
|
UpdateEntityFeature,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .common import AvmWrapper, FritzBoxBaseEntity
|
from .common import AvmWrapper, FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):
|
||||||
|
"""Describes Fritz update entity."""
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -27,11 +38,13 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity):
|
class FritzBoxUpdateEntity(FritzBoxBaseCoordinatorEntity, UpdateEntity):
|
||||||
"""Mixin for update entity specific attributes."""
|
"""Mixin for update entity specific attributes."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||||
_attr_title = "FRITZ!OS"
|
_attr_title = "FRITZ!OS"
|
||||||
|
entity_description: FritzUpdateEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -39,29 +52,30 @@ class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity):
|
|||||||
device_friendly_name: str,
|
device_friendly_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init FRITZ!Box connectivity class."""
|
"""Init FRITZ!Box connectivity class."""
|
||||||
self._attr_name = f"{device_friendly_name} FRITZ!OS"
|
description = FritzUpdateEntityDescription(
|
||||||
self._attr_unique_id = f"{avm_wrapper.unique_id}-update"
|
key="update", name="FRITZ!OS", value_fn=None
|
||||||
super().__init__(avm_wrapper, device_friendly_name)
|
)
|
||||||
|
super().__init__(avm_wrapper, device_friendly_name, description)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def installed_version(self) -> str | None:
|
def installed_version(self) -> str | None:
|
||||||
"""Version currently in use."""
|
"""Version currently in use."""
|
||||||
return self._avm_wrapper.current_firmware
|
return self.coordinator.current_firmware
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> str | None:
|
def latest_version(self) -> str | None:
|
||||||
"""Latest version available for install."""
|
"""Latest version available for install."""
|
||||||
if self._avm_wrapper.update_available:
|
if self.coordinator.update_available:
|
||||||
return self._avm_wrapper.latest_firmware
|
return self.coordinator.latest_firmware
|
||||||
return self._avm_wrapper.current_firmware
|
return self.coordinator.current_firmware
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def release_url(self) -> str | None:
|
def release_url(self) -> str | None:
|
||||||
"""URL to the full release notes of the latest version available."""
|
"""URL to the full release notes of the latest version available."""
|
||||||
return self._avm_wrapper.release_url
|
return self.coordinator.release_url
|
||||||
|
|
||||||
async def async_install(
|
async def async_install(
|
||||||
self, version: str | None, backup: bool, **kwargs: Any
|
self, version: str | None, backup: bool, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Install an update."""
|
"""Install an update."""
|
||||||
await self._avm_wrapper.async_trigger_firmware_update()
|
await self.coordinator.async_trigger_firmware_update()
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20230908.0"]
|
"requirements": ["home-assistant-frontend==20230911.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ from contextlib import suppress
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -149,10 +150,12 @@ SERVICE_BACKUP_PARTIAL = "backup_partial"
|
|||||||
SERVICE_RESTORE_FULL = "restore_full"
|
SERVICE_RESTORE_FULL = "restore_full"
|
||||||
SERVICE_RESTORE_PARTIAL = "restore_partial"
|
SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||||
|
|
||||||
|
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||||
|
|
||||||
|
|
||||||
def valid_addon(value: Any) -> str:
|
def valid_addon(value: Any) -> str:
|
||||||
"""Validate value is a valid addon slug."""
|
"""Validate value is a valid addon slug."""
|
||||||
value = cv.slug(value)
|
value = VALID_ADDON_SLUG(value)
|
||||||
|
|
||||||
hass: HomeAssistant | None = None
|
hass: HomeAssistant | None = None
|
||||||
with suppress(HomeAssistantError):
|
with suppress(HomeAssistantError):
|
||||||
|
@ -116,8 +116,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
name = discovery_info.name.replace(f".{zctype}", "")
|
name = discovery_info.name.replace(f".{zctype}", "")
|
||||||
tls = zctype == "_ipps._tcp.local."
|
tls = zctype == "_ipps._tcp.local."
|
||||||
base_path = discovery_info.properties.get("rp", "ipp/print")
|
base_path = discovery_info.properties.get("rp", "ipp/print")
|
||||||
|
unique_id = discovery_info.properties.get("UUID")
|
||||||
self.context.update({"title_placeholders": {"name": name}})
|
|
||||||
|
|
||||||
self.discovery_info.update(
|
self.discovery_info.update(
|
||||||
{
|
{
|
||||||
@ -127,10 +126,18 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_VERIFY_SSL: False,
|
CONF_VERIFY_SSL: False,
|
||||||
CONF_BASE_PATH: f"/{base_path}",
|
CONF_BASE_PATH: f"/{base_path}",
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
CONF_UUID: discovery_info.properties.get("UUID"),
|
CONF_UUID: unique_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if unique_id:
|
||||||
|
# If we already have the unique id, try to set it now
|
||||||
|
# so we can avoid probing the device if its already
|
||||||
|
# configured or ignored
|
||||||
|
await self._async_set_unique_id_and_abort_if_already_configured(unique_id)
|
||||||
|
|
||||||
|
self.context.update({"title_placeholders": {"name": name}})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = await validate_input(self.hass, self.discovery_info)
|
info = await validate_input(self.hass, self.discovery_info)
|
||||||
except IPPConnectionUpgradeRequired:
|
except IPPConnectionUpgradeRequired:
|
||||||
@ -147,7 +154,6 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("IPP Error", exc_info=True)
|
_LOGGER.debug("IPP Error", exc_info=True)
|
||||||
return self.async_abort(reason="ipp_error")
|
return self.async_abort(reason="ipp_error")
|
||||||
|
|
||||||
unique_id = self.discovery_info[CONF_UUID]
|
|
||||||
if not unique_id and info[CONF_UUID]:
|
if not unique_id and info[CONF_UUID]:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Printer UUID is missing from discovery info. Falling back to IPP UUID"
|
"Printer UUID is missing from discovery info. Falling back to IPP UUID"
|
||||||
@ -164,7 +170,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"Unable to determine unique id from discovery info and IPP response"
|
"Unable to determine unique id from discovery info and IPP response"
|
||||||
)
|
)
|
||||||
|
|
||||||
if unique_id:
|
if unique_id and self.unique_id != unique_id:
|
||||||
|
await self._async_set_unique_id_and_abort_if_already_configured(unique_id)
|
||||||
|
|
||||||
|
await self._async_handle_discovery_without_unique_id()
|
||||||
|
return await self.async_step_zeroconf_confirm()
|
||||||
|
|
||||||
|
async def _async_set_unique_id_and_abort_if_already_configured(
|
||||||
|
self, unique_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Set the unique ID and abort if already configured."""
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
updates={
|
updates={
|
||||||
@ -173,9 +188,6 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._async_handle_discovery_without_unique_id()
|
|
||||||
return await self.async_step_zeroconf_confirm()
|
|
||||||
|
|
||||||
async def async_step_zeroconf_confirm(
|
async def async_step_zeroconf_confirm(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["usb"],
|
"dependencies": ["usb"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter",
|
"documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["ultraheat-api==0.5.1"]
|
"requirements": ["ultraheat-api==0.5.7"]
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
|
|||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ACTIVE_SCAN_INTERVAL,
|
|
||||||
CALL_TYPE_COIL,
|
CALL_TYPE_COIL,
|
||||||
CALL_TYPE_DISCRETE,
|
CALL_TYPE_DISCRETE,
|
||||||
CALL_TYPE_REGISTER_HOLDING,
|
CALL_TYPE_REGISTER_HOLDING,
|
||||||
@ -116,8 +115,9 @@ class BasePlatform(Entity):
|
|||||||
def async_run(self) -> None:
|
def async_run(self) -> None:
|
||||||
"""Remote start entity."""
|
"""Remote start entity."""
|
||||||
self.async_hold(update=False)
|
self.async_hold(update=False)
|
||||||
if self._scan_interval == 0 or self._scan_interval > ACTIVE_SCAN_INTERVAL:
|
self._cancel_call = async_call_later(
|
||||||
self._cancel_call = async_call_later(self.hass, 1, self.async_update)
|
self.hass, timedelta(milliseconds=100), self.async_update
|
||||||
|
)
|
||||||
if self._scan_interval > 0:
|
if self._scan_interval > 0:
|
||||||
self._cancel_timer = async_track_time_interval(
|
self._cancel_timer = async_track_time_interval(
|
||||||
self.hass, self.async_update, timedelta(seconds=self._scan_interval)
|
self.hass, self.async_update, timedelta(seconds=self._scan_interval)
|
||||||
@ -188,10 +188,14 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
|
|||||||
registers.reverse()
|
registers.reverse()
|
||||||
return registers
|
return registers
|
||||||
|
|
||||||
def __process_raw_value(self, entry: float | int | str) -> float | int | str | None:
|
def __process_raw_value(
|
||||||
|
self, entry: float | int | str | bytes
|
||||||
|
) -> float | int | str | bytes | None:
|
||||||
"""Process value from sensor with NaN handling, scaling, offset, min/max etc."""
|
"""Process value from sensor with NaN handling, scaling, offset, min/max etc."""
|
||||||
if self._nan_value and entry in (self._nan_value, -self._nan_value):
|
if self._nan_value and entry in (self._nan_value, -self._nan_value):
|
||||||
return None
|
return None
|
||||||
|
if isinstance(entry, bytes):
|
||||||
|
return entry
|
||||||
val: float | int = self._scale * entry + self._offset
|
val: float | int = self._scale * entry + self._offset
|
||||||
if self._min_value is not None and val < self._min_value:
|
if self._min_value is not None and val < self._min_value:
|
||||||
return self._min_value
|
return self._min_value
|
||||||
@ -232,14 +236,20 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
|
|||||||
if isinstance(v_temp, int) and self._precision == 0:
|
if isinstance(v_temp, int) and self._precision == 0:
|
||||||
v_result.append(str(v_temp))
|
v_result.append(str(v_temp))
|
||||||
elif v_temp is None:
|
elif v_temp is None:
|
||||||
v_result.append("") # pragma: no cover
|
v_result.append("0")
|
||||||
elif v_temp != v_temp: # noqa: PLR0124
|
elif v_temp != v_temp: # noqa: PLR0124
|
||||||
# NaN float detection replace with None
|
# NaN float detection replace with None
|
||||||
v_result.append("nan") # pragma: no cover
|
v_result.append("0")
|
||||||
else:
|
else:
|
||||||
v_result.append(f"{float(v_temp):.{self._precision}f}")
|
v_result.append(f"{float(v_temp):.{self._precision}f}")
|
||||||
return ",".join(map(str, v_result))
|
return ",".join(map(str, v_result))
|
||||||
|
|
||||||
|
# NaN float detection replace with None
|
||||||
|
if val[0] != val[0]: # noqa: PLR0124
|
||||||
|
return None
|
||||||
|
if byte_string == b"nan\x00":
|
||||||
|
return None
|
||||||
|
|
||||||
# Apply scale, precision, limits to floats and ints
|
# Apply scale, precision, limits to floats and ints
|
||||||
val_result = self.__process_raw_value(val[0])
|
val_result = self.__process_raw_value(val[0])
|
||||||
|
|
||||||
@ -249,15 +259,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
|
|||||||
|
|
||||||
if val_result is None:
|
if val_result is None:
|
||||||
return None
|
return None
|
||||||
# NaN float detection replace with None
|
|
||||||
if val_result != val_result: # noqa: PLR0124
|
|
||||||
return None # pragma: no cover
|
|
||||||
if isinstance(val_result, int) and self._precision == 0:
|
if isinstance(val_result, int) and self._precision == 0:
|
||||||
return str(val_result)
|
return str(val_result)
|
||||||
if isinstance(val_result, str):
|
if isinstance(val_result, bytes):
|
||||||
if val_result == "nan":
|
return val_result.decode()
|
||||||
val_result = None # pragma: no cover
|
|
||||||
return val_result
|
|
||||||
return f"{float(val_result):.{self._precision}f}"
|
return f"{float(val_result):.{self._precision}f}"
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pymodbus"],
|
"loggers": ["pymodbus"],
|
||||||
"quality_scale": "gold",
|
"quality_scale": "gold",
|
||||||
"requirements": ["pymodbus==3.5.1"]
|
"requirements": ["pymodbus==3.5.2"]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Support for Modbus Register sensors."""
|
"""Support for Modbus Register sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
@ -106,12 +107,16 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
|
|||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
# remark "now" is a dummy parameter to avoid problems with
|
# remark "now" is a dummy parameter to avoid problems with
|
||||||
# async_track_time_interval
|
# async_track_time_interval
|
||||||
|
self._cancel_call = None
|
||||||
raw_result = await self._hub.async_pb_call(
|
raw_result = await self._hub.async_pb_call(
|
||||||
self._slave, self._address, self._count, self._input_type
|
self._slave, self._address, self._count, self._input_type
|
||||||
)
|
)
|
||||||
if raw_result is None:
|
if raw_result is None:
|
||||||
if self._lazy_errors:
|
if self._lazy_errors:
|
||||||
self._lazy_errors -= 1
|
self._lazy_errors -= 1
|
||||||
|
self._cancel_call = async_call_later(
|
||||||
|
self.hass, timedelta(seconds=1), self.async_update
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self._lazy_errors = self._lazy_error_count
|
self._lazy_errors = self._lazy_error_count
|
||||||
self._attr_available = False
|
self._attr_available = False
|
||||||
|
@ -1135,6 +1135,11 @@ class MqttEntity(
|
|||||||
elif not self._default_to_device_class_name():
|
elif not self._default_to_device_class_name():
|
||||||
# Assign the default name
|
# Assign the default name
|
||||||
self._attr_name = self._default_name
|
self._attr_name = self._default_name
|
||||||
|
elif hasattr(self, "_attr_name"):
|
||||||
|
# An entity name was not set in the config
|
||||||
|
# don't set the name attribute and derive
|
||||||
|
# the name from the device_class
|
||||||
|
delattr(self, "_attr_name")
|
||||||
if CONF_DEVICE in config:
|
if CONF_DEVICE in config:
|
||||||
device_name: str
|
device_name: str
|
||||||
if CONF_NAME not in config[CONF_DEVICE]:
|
if CONF_NAME not in config[CONF_DEVICE]:
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["crcmod", "plugwise"],
|
"loggers": ["crcmod", "plugwise"],
|
||||||
"requirements": ["plugwise==0.31.9"],
|
"requirements": ["plugwise==0.32.2"],
|
||||||
"zeroconf": ["_plugwise._tcp.local."]
|
"zeroconf": ["_plugwise._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ from .entity import PlugwiseEntity
|
|||||||
class PlugwiseEntityDescriptionMixin:
|
class PlugwiseEntityDescriptionMixin:
|
||||||
"""Mixin values for Plugwise entities."""
|
"""Mixin values for Plugwise entities."""
|
||||||
|
|
||||||
command: Callable[[Smile, str, float], Awaitable[None]]
|
command: Callable[[Smile, str, str, float], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -43,7 +43,9 @@ NUMBER_TYPES = (
|
|||||||
PlugwiseNumberEntityDescription(
|
PlugwiseNumberEntityDescription(
|
||||||
key="maximum_boiler_temperature",
|
key="maximum_boiler_temperature",
|
||||||
translation_key="maximum_boiler_temperature",
|
translation_key="maximum_boiler_temperature",
|
||||||
command=lambda api, number, value: api.set_number_setpoint(number, value),
|
command=lambda api, number, dev_id, value: api.set_number_setpoint(
|
||||||
|
number, dev_id, value
|
||||||
|
),
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
device_class=NumberDeviceClass.TEMPERATURE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
@ -51,7 +53,9 @@ NUMBER_TYPES = (
|
|||||||
PlugwiseNumberEntityDescription(
|
PlugwiseNumberEntityDescription(
|
||||||
key="max_dhw_temperature",
|
key="max_dhw_temperature",
|
||||||
translation_key="max_dhw_temperature",
|
translation_key="max_dhw_temperature",
|
||||||
command=lambda api, number, value: api.set_number_setpoint(number, value),
|
command=lambda api, number, dev_id, value: api.set_number_setpoint(
|
||||||
|
number, dev_id, value
|
||||||
|
),
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
device_class=NumberDeviceClass.TEMPERATURE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
@ -94,6 +98,7 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initiate Plugwise Number."""
|
"""Initiate Plugwise Number."""
|
||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
|
self.device_id = device_id
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{device_id}-{description.key}"
|
self._attr_unique_id = f"{device_id}-{description.key}"
|
||||||
self._attr_mode = NumberMode.BOX
|
self._attr_mode = NumberMode.BOX
|
||||||
@ -109,6 +114,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
|||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Change to the new setpoint value."""
|
"""Change to the new setpoint value."""
|
||||||
await self.entity_description.command(
|
await self.entity_description.command(
|
||||||
self.coordinator.api, self.entity_description.key, value
|
self.coordinator.api, self.entity_description.key, self.device_id, value
|
||||||
)
|
)
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/python_script",
|
"documentation": "https://www.home-assistant.io/integrations/python_script",
|
||||||
"loggers": ["RestrictedPython"],
|
"loggers": ["RestrictedPython"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["RestrictedPython==6.1"]
|
"requirements": ["RestrictedPython==6.2"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["roborock"],
|
"loggers": ["roborock"],
|
||||||
"requirements": ["python-roborock==0.32.3"]
|
"requirements": ["python-roborock==0.33.2"]
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,8 @@ class SomaTilt(SomaEntity, CoverEntity):
|
|||||||
| CoverEntityFeature.STOP_TILT
|
| CoverEntityFeature.STOP_TILT
|
||||||
| CoverEntityFeature.SET_TILT_POSITION
|
| CoverEntityFeature.SET_TILT_POSITION
|
||||||
)
|
)
|
||||||
|
CLOSED_UP_THRESHOLD = 80
|
||||||
|
CLOSED_DOWN_THRESHOLD = 20
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_cover_tilt_position(self) -> int:
|
def current_cover_tilt_position(self) -> int:
|
||||||
@ -60,7 +62,12 @@ class SomaTilt(SomaEntity, CoverEntity):
|
|||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
"""Return if the cover tilt is closed."""
|
"""Return if the cover tilt is closed."""
|
||||||
return self.current_position == 0
|
if (
|
||||||
|
self.current_position < self.CLOSED_DOWN_THRESHOLD
|
||||||
|
or self.current_position > self.CLOSED_UP_THRESHOLD
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def close_cover_tilt(self, **kwargs: Any) -> None:
|
def close_cover_tilt(self, **kwargs: Any) -> None:
|
||||||
"""Close the cover tilt."""
|
"""Close the cover tilt."""
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["systembridgeconnector"],
|
"loggers": ["systembridgeconnector"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["systembridgeconnector==3.4.9"],
|
"requirements": ["systembridgeconnector==3.8.2"],
|
||||||
"zeroconf": ["_system-bridge._tcp.local."]
|
"zeroconf": ["_system-bridge._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["hatasmota"],
|
"loggers": ["hatasmota"],
|
||||||
"mqtt": ["tasmota/discovery/#"],
|
"mqtt": ["tasmota/discovery/#"],
|
||||||
"requirements": ["HATasmota==0.7.1"]
|
"requirements": ["HATasmota==0.7.3"]
|
||||||
}
|
}
|
||||||
|
@ -88,12 +88,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
|
hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
|
||||||
hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
|
hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
|
||||||
hc.SENSOR_CURRENT: {
|
hc.SENSOR_CURRENT: {
|
||||||
ICON: "mdi:alpha-a-circle-outline",
|
|
||||||
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_CURRENTNEUTRAL: {
|
hc.SENSOR_CURRENTNEUTRAL: {
|
||||||
ICON: "mdi:alpha-a-circle-outline",
|
|
||||||
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
@ -103,11 +101,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_DISTANCE: {
|
hc.SENSOR_DISTANCE: {
|
||||||
ICON: "mdi:leak",
|
|
||||||
DEVICE_CLASS: SensorDeviceClass.DISTANCE,
|
DEVICE_CLASS: SensorDeviceClass.DISTANCE,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
|
hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
|
||||||
|
hc.SENSOR_ENERGY: {
|
||||||
|
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||||
|
STATE_CLASS: SensorStateClass.TOTAL,
|
||||||
|
},
|
||||||
hc.SENSOR_FREQUENCY: {
|
hc.SENSOR_FREQUENCY: {
|
||||||
DEVICE_CLASS: SensorDeviceClass.FREQUENCY,
|
DEVICE_CLASS: SensorDeviceClass.FREQUENCY,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
@ -122,10 +123,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
},
|
},
|
||||||
hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
|
hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
|
||||||
hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
|
hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
|
||||||
hc.SENSOR_MOISTURE: {
|
hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE},
|
||||||
DEVICE_CLASS: SensorDeviceClass.MOISTURE,
|
|
||||||
ICON: "mdi:cup-water",
|
|
||||||
},
|
|
||||||
hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
|
hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
|
||||||
hc.SENSOR_PB0_3: {ICON: "mdi:flask"},
|
hc.SENSOR_PB0_3: {ICON: "mdi:flask"},
|
||||||
hc.SENSOR_PB0_5: {ICON: "mdi:flask"},
|
hc.SENSOR_PB0_5: {ICON: "mdi:flask"},
|
||||||
@ -146,7 +144,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_POWERFACTOR: {
|
hc.SENSOR_POWERFACTOR: {
|
||||||
ICON: "mdi:alpha-f-circle-outline",
|
|
||||||
DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
|
DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
@ -162,7 +159,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
|
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_PROXIMITY: {DEVICE_CLASS: SensorDeviceClass.DISTANCE, ICON: "mdi:ruler"},
|
hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
|
||||||
hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||||
hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||||
hc.SENSOR_REACTIVE_POWERUSAGE: {
|
hc.SENSOR_REACTIVE_POWERUSAGE: {
|
||||||
@ -195,11 +192,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
|||||||
hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
|
hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
|
||||||
hc.SENSOR_TVOC: {ICON: "mdi:air-filter"},
|
hc.SENSOR_TVOC: {ICON: "mdi:air-filter"},
|
||||||
hc.SENSOR_VOLTAGE: {
|
hc.SENSOR_VOLTAGE: {
|
||||||
ICON: "mdi:alpha-v-circle-outline",
|
DEVICE_CLASS: SensorDeviceClass.VOLTAGE,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
hc.SENSOR_WEIGHT: {
|
hc.SENSOR_WEIGHT: {
|
||||||
ICON: "mdi:scale",
|
|
||||||
DEVICE_CLASS: SensorDeviceClass.WEIGHT,
|
DEVICE_CLASS: SensorDeviceClass.WEIGHT,
|
||||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
@ -220,7 +216,6 @@ SENSOR_UNIT_MAP = {
|
|||||||
hc.LIGHT_LUX: LIGHT_LUX,
|
hc.LIGHT_LUX: LIGHT_LUX,
|
||||||
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
|
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
|
||||||
hc.PERCENTAGE: PERCENTAGE,
|
hc.PERCENTAGE: PERCENTAGE,
|
||||||
hc.POWER_FACTOR: None,
|
|
||||||
hc.POWER_WATT: UnitOfPower.WATT,
|
hc.POWER_WATT: UnitOfPower.WATT,
|
||||||
hc.PRESSURE_HPA: UnitOfPressure.HPA,
|
hc.PRESSURE_HPA: UnitOfPressure.HPA,
|
||||||
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
|
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
|
||||||
|
@ -23,8 +23,7 @@ class TriggerEntity(TriggerBaseEntity, CoordinatorEntity[TriggerUpdateCoordinato
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle being added to Home Assistant."""
|
"""Handle being added to Home Assistant."""
|
||||||
await TriggerBaseEntity.async_added_to_hass(self)
|
await super().async_added_to_hass()
|
||||||
await CoordinatorEntity.async_added_to_hass(self) # type: ignore[arg-type]
|
|
||||||
if self.coordinator.data is not None:
|
if self.coordinator.data is not None:
|
||||||
self._process_data()
|
self._process_data()
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class UnifiEntityTrackerDescriptionMixin(Generic[HandlerT, ApiItemT]):
|
|||||||
"""Device tracker local functions."""
|
"""Device tracker local functions."""
|
||||||
|
|
||||||
heartbeat_timedelta_fn: Callable[[UniFiController, str], timedelta]
|
heartbeat_timedelta_fn: Callable[[UniFiController, str], timedelta]
|
||||||
ip_address_fn: Callable[[aiounifi.Controller, str], str]
|
ip_address_fn: Callable[[aiounifi.Controller, str], str | None]
|
||||||
is_connected_fn: Callable[[UniFiController, str], bool]
|
is_connected_fn: Callable[[UniFiController, str], bool]
|
||||||
hostname_fn: Callable[[aiounifi.Controller, str], str | None]
|
hostname_fn: Callable[[aiounifi.Controller, str], str | None]
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
|
|||||||
return self.entity_description.hostname_fn(self.controller.api, self._obj_id)
|
return self.entity_description.hostname_fn(self.controller.api, self._obj_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> str:
|
def ip_address(self) -> str | None:
|
||||||
"""Return the primary ip address of the device."""
|
"""Return the primary ip address of the device."""
|
||||||
return self.entity_description.ip_address_fn(self.controller.api, self._obj_id)
|
return self.entity_description.ip_address_fn(self.controller.api, self._obj_id)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiounifi"],
|
"loggers": ["aiounifi"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiounifi==61"],
|
"requirements": ["aiounifi==62"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Ubiquiti Networks",
|
"manufacturer": "Ubiquiti Networks",
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/vodafone_station",
|
"documentation": "https://www.home-assistant.io/integrations/vodafone_station",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiovodafone"],
|
"loggers": ["aiovodafone"],
|
||||||
"requirements": ["aiovodafone==0.1.0"]
|
"requirements": ["aiovodafone==0.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
|
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pywaze", "homeassistant.helpers.location"],
|
"loggers": ["pywaze", "homeassistant.helpers.location"],
|
||||||
"requirements": ["pywaze==0.3.0"]
|
"requirements": ["pywaze==0.4.0"]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Support for Zigbee Home Automation devices."""
|
"""Support for Zigbee Home Automation devices."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -33,13 +34,16 @@ from .core.const import (
|
|||||||
CONF_ZIGPY,
|
CONF_ZIGPY,
|
||||||
DATA_ZHA,
|
DATA_ZHA,
|
||||||
DATA_ZHA_CONFIG,
|
DATA_ZHA_CONFIG,
|
||||||
|
DATA_ZHA_DEVICE_TRIGGER_CACHE,
|
||||||
DATA_ZHA_GATEWAY,
|
DATA_ZHA_GATEWAY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
RadioType,
|
RadioType,
|
||||||
)
|
)
|
||||||
|
from .core.device import get_device_automation_triggers
|
||||||
from .core.discovery import GROUP_PROBE
|
from .core.discovery import GROUP_PROBE
|
||||||
|
from .radio_manager import ZhaRadioManager
|
||||||
|
|
||||||
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string})
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string})
|
||||||
ZHA_CONFIG_SCHEMA = {
|
ZHA_CONFIG_SCHEMA = {
|
||||||
@ -134,10 +138,44 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
else:
|
else:
|
||||||
_LOGGER.debug("ZHA storage file does not exist or was already removed")
|
_LOGGER.debug("ZHA storage file does not exist or was already removed")
|
||||||
|
|
||||||
# Re-use the gateway object between ZHA reloads
|
# Load and cache device trigger information early
|
||||||
if (zha_gateway := zha_data.get(DATA_ZHA_GATEWAY)) is None:
|
zha_data.setdefault(DATA_ZHA_DEVICE_TRIGGER_CACHE, {})
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry)
|
||||||
|
|
||||||
|
async with radio_mgr.connect_zigpy_app() as app:
|
||||||
|
for dev in app.devices.values():
|
||||||
|
dev_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, str(dev.ieee))},
|
||||||
|
connections={(dr.CONNECTION_ZIGBEE, str(dev.ieee))},
|
||||||
|
)
|
||||||
|
|
||||||
|
if dev_entry is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
zha_data[DATA_ZHA_DEVICE_TRIGGER_CACHE][dev_entry.id] = (
|
||||||
|
str(dev.ieee),
|
||||||
|
get_device_automation_triggers(dev),
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Trigger cache: %s", zha_data[DATA_ZHA_DEVICE_TRIGGER_CACHE])
|
||||||
|
|
||||||
zha_gateway = ZHAGateway(hass, config, config_entry)
|
zha_gateway = ZHAGateway(hass, config, config_entry)
|
||||||
|
|
||||||
|
async def async_zha_shutdown():
|
||||||
|
"""Handle shutdown tasks."""
|
||||||
|
await zha_gateway.shutdown()
|
||||||
|
# clean up any remaining entity metadata
|
||||||
|
# (entities that have been discovered but not yet added to HA)
|
||||||
|
# suppress KeyError because we don't know what state we may
|
||||||
|
# be in when we get here in failure cases
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
del hass.data[DATA_ZHA][platform]
|
||||||
|
|
||||||
|
config_entry.async_on_unload(async_zha_shutdown)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await zha_gateway.async_initialize()
|
await zha_gateway.async_initialize()
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
@ -155,9 +193,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
|
|
||||||
repairs.async_delete_blocking_issues(hass)
|
repairs.async_delete_blocking_issues(hass)
|
||||||
|
|
||||||
config_entry.async_on_unload(zha_gateway.shutdown)
|
|
||||||
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))},
|
connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))},
|
||||||
|
@ -186,6 +186,7 @@ DATA_ZHA = "zha"
|
|||||||
DATA_ZHA_CONFIG = "config"
|
DATA_ZHA_CONFIG = "config"
|
||||||
DATA_ZHA_BRIDGE_ID = "zha_bridge_id"
|
DATA_ZHA_BRIDGE_ID = "zha_bridge_id"
|
||||||
DATA_ZHA_CORE_EVENTS = "zha_core_events"
|
DATA_ZHA_CORE_EVENTS = "zha_core_events"
|
||||||
|
DATA_ZHA_DEVICE_TRIGGER_CACHE = "zha_device_trigger_cache"
|
||||||
DATA_ZHA_GATEWAY = "zha_gateway"
|
DATA_ZHA_GATEWAY = "zha_gateway"
|
||||||
|
|
||||||
DEBUG_COMP_BELLOWS = "bellows"
|
DEBUG_COMP_BELLOWS = "bellows"
|
||||||
|
@ -93,6 +93,16 @@ _UPDATE_ALIVE_INTERVAL = (60, 90)
|
|||||||
_CHECKIN_GRACE_PERIODS = 2
|
_CHECKIN_GRACE_PERIODS = 2
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_automation_triggers(
|
||||||
|
device: zigpy.device.Device,
|
||||||
|
) -> dict[tuple[str, str], dict[str, str]]:
|
||||||
|
"""Get the supported device automation triggers for a zigpy device."""
|
||||||
|
return {
|
||||||
|
("device_offline", "device_offline"): {"device_event_type": "device_offline"},
|
||||||
|
**getattr(device, "device_automation_triggers", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceStatus(Enum):
|
class DeviceStatus(Enum):
|
||||||
"""Status of a device."""
|
"""Status of a device."""
|
||||||
|
|
||||||
@ -311,16 +321,7 @@ class ZHADevice(LogMixin):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]:
|
def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]:
|
||||||
"""Return the device automation triggers for this device."""
|
"""Return the device automation triggers for this device."""
|
||||||
triggers = {
|
return get_device_automation_triggers(self._zigpy_device)
|
||||||
("device_offline", "device_offline"): {
|
|
||||||
"device_event_type": "device_offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasattr(self._zigpy_device, "device_automation_triggers"):
|
|
||||||
triggers.update(self._zigpy_device.device_automation_triggers)
|
|
||||||
|
|
||||||
return triggers
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_signal(self) -> str:
|
def available_signal(self) -> str:
|
||||||
|
@ -149,12 +149,6 @@ class ZHAGateway:
|
|||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
self._unsubs: list[Callable[[], None]] = []
|
self._unsubs: list[Callable[[], None]] = []
|
||||||
|
|
||||||
discovery.PROBE.initialize(self._hass)
|
|
||||||
discovery.GROUP_PROBE.initialize(self._hass)
|
|
||||||
|
|
||||||
self.ha_device_registry = dr.async_get(self._hass)
|
|
||||||
self.ha_entity_registry = er.async_get(self._hass)
|
|
||||||
|
|
||||||
def get_application_controller_data(self) -> tuple[ControllerApplication, dict]:
|
def get_application_controller_data(self) -> tuple[ControllerApplication, dict]:
|
||||||
"""Get an uninitialized instance of a zigpy `ControllerApplication`."""
|
"""Get an uninitialized instance of a zigpy `ControllerApplication`."""
|
||||||
radio_type = self.config_entry.data[CONF_RADIO_TYPE]
|
radio_type = self.config_entry.data[CONF_RADIO_TYPE]
|
||||||
@ -197,6 +191,12 @@ class ZHAGateway:
|
|||||||
|
|
||||||
async def async_initialize(self) -> None:
|
async def async_initialize(self) -> None:
|
||||||
"""Initialize controller and connect radio."""
|
"""Initialize controller and connect radio."""
|
||||||
|
discovery.PROBE.initialize(self._hass)
|
||||||
|
discovery.GROUP_PROBE.initialize(self._hass)
|
||||||
|
|
||||||
|
self.ha_device_registry = dr.async_get(self._hass)
|
||||||
|
self.ha_entity_registry = er.async_get(self._hass)
|
||||||
|
|
||||||
app_controller_cls, app_config = self.get_application_controller_data()
|
app_controller_cls, app_config = self.get_application_controller_data()
|
||||||
self.application_controller = await app_controller_cls.new(
|
self.application_controller = await app_controller_cls.new(
|
||||||
config=app_config,
|
config=app_config,
|
||||||
@ -204,23 +204,6 @@ class ZHAGateway:
|
|||||||
start_radio=False,
|
start_radio=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
|
|
||||||
|
|
||||||
self.async_load_devices()
|
|
||||||
|
|
||||||
# Groups are attached to the coordinator device so we need to load it early
|
|
||||||
coordinator = self._find_coordinator_device()
|
|
||||||
loaded_groups = False
|
|
||||||
|
|
||||||
# We can only load groups early if the coordinator's model info has been stored
|
|
||||||
# in the zigpy database
|
|
||||||
if coordinator.model is not None:
|
|
||||||
self.coordinator_zha_device = self._async_get_or_create_device(
|
|
||||||
coordinator, restored=True
|
|
||||||
)
|
|
||||||
self.async_load_groups()
|
|
||||||
loaded_groups = True
|
|
||||||
|
|
||||||
for attempt in range(STARTUP_RETRIES):
|
for attempt in range(STARTUP_RETRIES):
|
||||||
try:
|
try:
|
||||||
await self.application_controller.startup(auto_form=True)
|
await self.application_controller.startup(auto_form=True)
|
||||||
@ -242,13 +225,14 @@ class ZHAGateway:
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
|
||||||
|
self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee)
|
||||||
|
|
||||||
self.coordinator_zha_device = self._async_get_or_create_device(
|
self.coordinator_zha_device = self._async_get_or_create_device(
|
||||||
self._find_coordinator_device(), restored=True
|
self._find_coordinator_device(), restored=True
|
||||||
)
|
)
|
||||||
self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee)
|
|
||||||
|
|
||||||
# If ZHA groups could not load early, we can safely load them now
|
self.async_load_devices()
|
||||||
if not loaded_groups:
|
|
||||||
self.async_load_groups()
|
self.async_load_groups()
|
||||||
|
|
||||||
self.application_controller.add_listener(self)
|
self.application_controller.add_listener(self)
|
||||||
@ -766,6 +750,14 @@ class ZHAGateway:
|
|||||||
unsubscribe()
|
unsubscribe()
|
||||||
for device in self.devices.values():
|
for device in self.devices.values():
|
||||||
device.async_cleanup_handles()
|
device.async_cleanup_handles()
|
||||||
|
# shutdown is called when the config entry unloads are processed
|
||||||
|
# there are cases where unloads are processed because of a failure of
|
||||||
|
# some sort and the application controller may not have been
|
||||||
|
# created yet
|
||||||
|
if (
|
||||||
|
hasattr(self, "application_controller")
|
||||||
|
and self.application_controller is not None
|
||||||
|
):
|
||||||
await self.application_controller.shutdown()
|
await self.application_controller.shutdown()
|
||||||
|
|
||||||
def handle_message(
|
def handle_message(
|
||||||
|
@ -9,12 +9,12 @@ from homeassistant.components.device_automation.exceptions import (
|
|||||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, IntegrationError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import DOMAIN as ZHA_DOMAIN
|
from . import DOMAIN as ZHA_DOMAIN
|
||||||
from .core.const import ZHA_EVENT
|
from .core.const import DATA_ZHA, DATA_ZHA_DEVICE_TRIGGER_CACHE, ZHA_EVENT
|
||||||
from .core.helpers import async_get_zha_device
|
from .core.helpers import async_get_zha_device
|
||||||
|
|
||||||
CONF_SUBTYPE = "subtype"
|
CONF_SUBTYPE = "subtype"
|
||||||
@ -26,21 +26,32 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_device_trigger_data(hass: HomeAssistant, device_id: str) -> tuple[str, dict]:
|
||||||
|
"""Get device trigger data for a device, falling back to the cache if possible."""
|
||||||
|
|
||||||
|
# First, try checking to see if the device itself is accessible
|
||||||
|
try:
|
||||||
|
zha_device = async_get_zha_device(hass, device_id)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return str(zha_device.ieee), zha_device.device_automation_triggers
|
||||||
|
|
||||||
|
# If not, check the trigger cache but allow any `KeyError`s to propagate
|
||||||
|
return hass.data[DATA_ZHA][DATA_ZHA_DEVICE_TRIGGER_CACHE][device_id]
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_trigger_config(
|
async def async_validate_trigger_config(
|
||||||
hass: HomeAssistant, config: ConfigType
|
hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
config = TRIGGER_SCHEMA(config)
|
config = TRIGGER_SCHEMA(config)
|
||||||
|
|
||||||
|
# Trigger validation will not occur if the config entry is not loaded
|
||||||
|
_, triggers = _get_device_trigger_data(hass, config[CONF_DEVICE_ID])
|
||||||
|
|
||||||
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||||
try:
|
if trigger not in triggers:
|
||||||
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
|
||||||
except (KeyError, AttributeError, IntegrationError) as err:
|
|
||||||
raise InvalidDeviceAutomationConfig from err
|
|
||||||
if (
|
|
||||||
zha_device.device_automation_triggers is None
|
|
||||||
or trigger not in zha_device.device_automation_triggers
|
|
||||||
):
|
|
||||||
raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}")
|
raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@ -53,26 +64,26 @@ async def async_attach_trigger(
|
|||||||
trigger_info: TriggerInfo,
|
trigger_info: TriggerInfo,
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
|
||||||
try:
|
try:
|
||||||
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
ieee, triggers = _get_device_trigger_data(hass, config[CONF_DEVICE_ID])
|
||||||
except (KeyError, AttributeError) as err:
|
except KeyError as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Unable to get zha device {config[CONF_DEVICE_ID]}"
|
f"Unable to get zha device {config[CONF_DEVICE_ID]}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
if trigger_key not in zha_device.device_automation_triggers:
|
trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||||
|
|
||||||
|
if trigger_key not in triggers:
|
||||||
raise HomeAssistantError(f"Unable to find trigger {trigger_key}")
|
raise HomeAssistantError(f"Unable to find trigger {trigger_key}")
|
||||||
|
|
||||||
trigger = zha_device.device_automation_triggers[trigger_key]
|
event_config = event_trigger.TRIGGER_SCHEMA(
|
||||||
|
{
|
||||||
event_config = {
|
|
||||||
event_trigger.CONF_PLATFORM: "event",
|
event_trigger.CONF_PLATFORM: "event",
|
||||||
event_trigger.CONF_EVENT_TYPE: ZHA_EVENT,
|
event_trigger.CONF_EVENT_TYPE: ZHA_EVENT,
|
||||||
event_trigger.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger},
|
event_trigger.CONF_EVENT_DATA: {DEVICE_IEEE: ieee, **triggers[trigger_key]},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
|
||||||
return await event_trigger.async_attach_trigger(
|
return await event_trigger.async_attach_trigger(
|
||||||
hass, event_config, action, trigger_info, platform_type="device"
|
hass, event_config, action, trigger_info, platform_type="device"
|
||||||
)
|
)
|
||||||
@ -83,17 +94,14 @@ async def async_get_triggers(
|
|||||||
) -> list[dict[str, str]]:
|
) -> list[dict[str, str]]:
|
||||||
"""List device triggers.
|
"""List device triggers.
|
||||||
|
|
||||||
Make sure the device supports device automations and
|
Make sure the device supports device automations and return the trigger list.
|
||||||
if it does return the trigger list.
|
|
||||||
"""
|
"""
|
||||||
zha_device = async_get_zha_device(hass, device_id)
|
try:
|
||||||
|
_, triggers = _get_device_trigger_data(hass, device_id)
|
||||||
|
except KeyError as err:
|
||||||
|
raise InvalidDeviceAutomationConfig from err
|
||||||
|
|
||||||
if not zha_device.device_automation_triggers:
|
return [
|
||||||
return []
|
|
||||||
|
|
||||||
triggers = []
|
|
||||||
for trigger, subtype in zha_device.device_automation_triggers:
|
|
||||||
triggers.append(
|
|
||||||
{
|
{
|
||||||
CONF_DEVICE_ID: device_id,
|
CONF_DEVICE_ID: device_id,
|
||||||
CONF_DOMAIN: ZHA_DOMAIN,
|
CONF_DOMAIN: ZHA_DOMAIN,
|
||||||
@ -101,6 +109,5 @@ async def async_get_triggers(
|
|||||||
CONF_TYPE: trigger,
|
CONF_TYPE: trigger,
|
||||||
CONF_SUBTYPE: subtype,
|
CONF_SUBTYPE: subtype,
|
||||||
}
|
}
|
||||||
)
|
for trigger, subtype in triggers
|
||||||
|
]
|
||||||
return triggers
|
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
"pyserial==3.5",
|
"pyserial==3.5",
|
||||||
"pyserial-asyncio==0.6",
|
"pyserial-asyncio==0.6",
|
||||||
"zha-quirks==0.0.103",
|
"zha-quirks==0.0.103",
|
||||||
"zigpy-deconz==0.21.0",
|
"zigpy-deconz==0.21.1",
|
||||||
"zigpy==0.57.1",
|
"zigpy==0.57.1",
|
||||||
"zigpy-xbee==0.18.1",
|
"zigpy-xbee==0.18.2",
|
||||||
"zigpy-zigate==0.11.0",
|
"zigpy-zigate==0.11.0",
|
||||||
"zigpy-znp==0.11.4",
|
"zigpy-znp==0.11.4",
|
||||||
"universal-silabs-flasher==0.0.13"
|
"universal-silabs-flasher==0.0.13"
|
||||||
|
@ -8,7 +8,7 @@ import copy
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any, Self
|
||||||
|
|
||||||
from bellows.config import CONF_USE_THREAD
|
from bellows.config import CONF_USE_THREAD
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -127,8 +127,21 @@ class ZhaRadioManager:
|
|||||||
self.backups: list[zigpy.backups.NetworkBackup] = []
|
self.backups: list[zigpy.backups.NetworkBackup] = []
|
||||||
self.chosen_backup: zigpy.backups.NetworkBackup | None = None
|
self.chosen_backup: zigpy.backups.NetworkBackup | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config_entry(
|
||||||
|
cls, hass: HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||||
|
) -> Self:
|
||||||
|
"""Create an instance from a config entry."""
|
||||||
|
mgr = cls()
|
||||||
|
mgr.hass = hass
|
||||||
|
mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||||
|
mgr.device_settings = config_entry.data[CONF_DEVICE]
|
||||||
|
mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]
|
||||||
|
|
||||||
|
return mgr
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def _connect_zigpy_app(self) -> ControllerApplication:
|
async def connect_zigpy_app(self) -> ControllerApplication:
|
||||||
"""Connect to the radio with the current config and then clean up."""
|
"""Connect to the radio with the current config and then clean up."""
|
||||||
assert self.radio_type is not None
|
assert self.radio_type is not None
|
||||||
|
|
||||||
@ -155,10 +168,9 @@ class ZhaRadioManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await app.connect()
|
|
||||||
yield app
|
yield app
|
||||||
finally:
|
finally:
|
||||||
await app.disconnect()
|
await app.shutdown()
|
||||||
await asyncio.sleep(CONNECT_DELAY_S)
|
await asyncio.sleep(CONNECT_DELAY_S)
|
||||||
|
|
||||||
async def restore_backup(
|
async def restore_backup(
|
||||||
@ -170,7 +182,8 @@ class ZhaRadioManager:
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
async with self._connect_zigpy_app() as app:
|
async with self.connect_zigpy_app() as app:
|
||||||
|
await app.connect()
|
||||||
await app.backups.restore_backup(backup, **kwargs)
|
await app.backups.restore_backup(backup, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -218,7 +231,9 @@ class ZhaRadioManager:
|
|||||||
"""Connect to the radio and load its current network settings."""
|
"""Connect to the radio and load its current network settings."""
|
||||||
backup = None
|
backup = None
|
||||||
|
|
||||||
async with self._connect_zigpy_app() as app:
|
async with self.connect_zigpy_app() as app:
|
||||||
|
await app.connect()
|
||||||
|
|
||||||
# Check if the stick has any settings and load them
|
# Check if the stick has any settings and load them
|
||||||
try:
|
try:
|
||||||
await app.load_network_info()
|
await app.load_network_info()
|
||||||
@ -241,12 +256,14 @@ class ZhaRadioManager:
|
|||||||
|
|
||||||
async def async_form_network(self) -> None:
|
async def async_form_network(self) -> None:
|
||||||
"""Form a brand-new network."""
|
"""Form a brand-new network."""
|
||||||
async with self._connect_zigpy_app() as app:
|
async with self.connect_zigpy_app() as app:
|
||||||
|
await app.connect()
|
||||||
await app.form_network()
|
await app.form_network()
|
||||||
|
|
||||||
async def async_reset_adapter(self) -> None:
|
async def async_reset_adapter(self) -> None:
|
||||||
"""Reset the current adapter."""
|
"""Reset the current adapter."""
|
||||||
async with self._connect_zigpy_app() as app:
|
async with self.connect_zigpy_app() as app:
|
||||||
|
await app.connect()
|
||||||
await app.reset_network_info()
|
await app.reset_network_info()
|
||||||
|
|
||||||
async def async_restore_backup_step_1(self) -> bool:
|
async def async_restore_backup_step_1(self) -> bool:
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["zwave_js_server"],
|
"loggers": ["zwave_js_server"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.1"],
|
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"],
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
"vid": "0658",
|
"vid": "0658",
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zwave_js_server.model.node import Node
|
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
||||||
@ -14,10 +13,10 @@ from .helpers import async_get_node_from_device_id
|
|||||||
class DeviceConfigFileChangedFlow(RepairsFlow):
|
class DeviceConfigFileChangedFlow(RepairsFlow):
|
||||||
"""Handler for an issue fixing flow."""
|
"""Handler for an issue fixing flow."""
|
||||||
|
|
||||||
def __init__(self, node: Node, device_name: str) -> None:
|
def __init__(self, data: dict[str, str]) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.node = node
|
self.device_name: str = data["device_name"]
|
||||||
self.device_name = device_name
|
self.device_id: str = data["device_id"]
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
@ -30,7 +29,14 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
|
|||||||
) -> data_entry_flow.FlowResult:
|
) -> data_entry_flow.FlowResult:
|
||||||
"""Handle the confirm step of a fix flow."""
|
"""Handle the confirm step of a fix flow."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.hass.async_create_task(self.node.async_refresh_info())
|
try:
|
||||||
|
node = async_get_node_from_device_id(self.hass, self.device_id)
|
||||||
|
except ValueError:
|
||||||
|
return self.async_abort(
|
||||||
|
reason="cannot_connect",
|
||||||
|
description_placeholders={"device_name": self.device_name},
|
||||||
|
)
|
||||||
|
self.hass.async_create_task(node.async_refresh_info())
|
||||||
return self.async_create_entry(title="", data={})
|
return self.async_create_entry(title="", data={})
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -41,15 +47,11 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
|
|||||||
|
|
||||||
|
|
||||||
async def async_create_fix_flow(
|
async def async_create_fix_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
||||||
issue_id: str,
|
|
||||||
data: dict[str, str] | None,
|
|
||||||
) -> RepairsFlow:
|
) -> RepairsFlow:
|
||||||
"""Create flow."""
|
"""Create flow."""
|
||||||
|
|
||||||
if issue_id.split(".")[0] == "device_config_file_changed":
|
if issue_id.split(".")[0] == "device_config_file_changed":
|
||||||
assert data
|
assert data
|
||||||
return DeviceConfigFileChangedFlow(
|
return DeviceConfigFileChangedFlow(data)
|
||||||
async_get_node_from_device_id(hass, data["device_id"]), data["device_name"]
|
|
||||||
)
|
|
||||||
return ConfirmRepairFlow()
|
return ConfirmRepairFlow()
|
||||||
|
@ -170,6 +170,9 @@
|
|||||||
"title": "Z-Wave device configuration file changed: {device_name}",
|
"title": "Z-Wave device configuration file changed: {device_name}",
|
||||||
"description": "Z-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background."
|
"description": "Z-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ from typing import Final
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2023
|
MAJOR_VERSION: Final = 2023
|
||||||
MINOR_VERSION: Final = 9
|
MINOR_VERSION: Final = 9
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -9,9 +9,9 @@ attrs==23.1.0
|
|||||||
awesomeversion==22.9.0
|
awesomeversion==22.9.0
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
bleak-retry-connector==3.1.3
|
bleak-retry-connector==3.1.3
|
||||||
bleak==0.21.0
|
bleak==0.21.1
|
||||||
bluetooth-adapters==0.16.1
|
bluetooth-adapters==0.16.1
|
||||||
bluetooth-auto-recovery==1.2.2
|
bluetooth-auto-recovery==1.2.3
|
||||||
bluetooth-data-tools==1.11.0
|
bluetooth-data-tools==1.11.0
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.3.0
|
ciso8601==2.3.0
|
||||||
@ -19,10 +19,10 @@ cryptography==41.0.3
|
|||||||
dbus-fast==1.95.2
|
dbus-fast==1.95.2
|
||||||
fnv-hash-fast==0.4.1
|
fnv-hash-fast==0.4.1
|
||||||
ha-av==10.1.1
|
ha-av==10.1.1
|
||||||
hass-nabucasa==0.70.0
|
hass-nabucasa==0.71.0
|
||||||
hassil==1.2.5
|
hassil==1.2.5
|
||||||
home-assistant-bluetooth==1.10.3
|
home-assistant-bluetooth==1.10.3
|
||||||
home-assistant-frontend==20230908.0
|
home-assistant-frontend==20230911.0
|
||||||
home-assistant-intents==2023.8.2
|
home-assistant-intents==2023.8.2
|
||||||
httpx==0.24.1
|
httpx==0.24.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2023.9.1"
|
version = "2023.9.2"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -29,7 +29,7 @@ DoorBirdPy==2.1.0
|
|||||||
HAP-python==4.7.1
|
HAP-python==4.7.1
|
||||||
|
|
||||||
# homeassistant.components.tasmota
|
# homeassistant.components.tasmota
|
||||||
HATasmota==0.7.1
|
HATasmota==0.7.3
|
||||||
|
|
||||||
# homeassistant.components.mastodon
|
# homeassistant.components.mastodon
|
||||||
Mastodon.py==1.5.1
|
Mastodon.py==1.5.1
|
||||||
@ -121,7 +121,7 @@ PyXiaomiGateway==0.14.3
|
|||||||
RachioPy==1.0.3
|
RachioPy==1.0.3
|
||||||
|
|
||||||
# homeassistant.components.python_script
|
# homeassistant.components.python_script
|
||||||
RestrictedPython==6.1
|
RestrictedPython==6.2
|
||||||
|
|
||||||
# homeassistant.components.remember_the_milk
|
# homeassistant.components.remember_the_milk
|
||||||
RtmAPI==0.7.2
|
RtmAPI==0.7.2
|
||||||
@ -363,13 +363,13 @@ aiosyncthing==0.5.1
|
|||||||
aiotractive==0.5.6
|
aiotractive==0.5.6
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==61
|
aiounifi==62
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.vodafone_station
|
# homeassistant.components.vodafone_station
|
||||||
aiovodafone==0.1.0
|
aiovodafone==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.waqi
|
# homeassistant.components.waqi
|
||||||
aiowaqi==0.2.1
|
aiowaqi==0.2.1
|
||||||
@ -521,7 +521,7 @@ bizkaibus==0.1.1
|
|||||||
bleak-retry-connector==3.1.3
|
bleak-retry-connector==3.1.3
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak==0.21.0
|
bleak==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox-uniapi==2.1.4
|
blebox-uniapi==2.1.4
|
||||||
@ -543,7 +543,7 @@ bluemaestro-ble==0.2.3
|
|||||||
bluetooth-adapters==0.16.1
|
bluetooth-adapters==0.16.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==1.2.2
|
bluetooth-auto-recovery==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
@ -958,7 +958,7 @@ ha-philipsjs==3.1.0
|
|||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.70.0
|
hass-nabucasa==0.71.0
|
||||||
|
|
||||||
# homeassistant.components.splunk
|
# homeassistant.components.splunk
|
||||||
hass-splunk==0.1.1
|
hass-splunk==0.1.1
|
||||||
@ -994,7 +994,7 @@ hole==0.8.0
|
|||||||
holidays==0.28
|
holidays==0.28
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20230908.0
|
home-assistant-frontend==20230911.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2023.8.2
|
home-assistant-intents==2023.8.2
|
||||||
@ -1438,7 +1438,7 @@ plexauth==0.0.6
|
|||||||
plexwebsocket==0.0.13
|
plexwebsocket==0.0.13
|
||||||
|
|
||||||
# homeassistant.components.plugwise
|
# homeassistant.components.plugwise
|
||||||
plugwise==0.31.9
|
plugwise==0.32.2
|
||||||
|
|
||||||
# homeassistant.components.plum_lightpad
|
# homeassistant.components.plum_lightpad
|
||||||
plumlightpad==0.0.11
|
plumlightpad==0.0.11
|
||||||
@ -1851,7 +1851,7 @@ pymitv==1.4.3
|
|||||||
pymochad==0.2.0
|
pymochad==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
# homeassistant.components.modbus
|
||||||
pymodbus==3.5.1
|
pymodbus==3.5.2
|
||||||
|
|
||||||
# homeassistant.components.monoprice
|
# homeassistant.components.monoprice
|
||||||
pymonoprice==0.4
|
pymonoprice==0.4
|
||||||
@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3
|
|||||||
python-ripple-api==0.0.3
|
python-ripple-api==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.roborock
|
# homeassistant.components.roborock
|
||||||
python-roborock==0.32.3
|
python-roborock==0.33.2
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.33
|
python-smarttub==0.0.33
|
||||||
@ -2231,7 +2231,7 @@ pyvlx==0.2.20
|
|||||||
pyvolumio==0.1.5
|
pyvolumio==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.waze_travel_time
|
# homeassistant.components.waze_travel_time
|
||||||
pywaze==0.3.0
|
pywaze==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
@ -2504,7 +2504,7 @@ swisshydrodata==0.1.0
|
|||||||
synology-srm==0.2.0
|
synology-srm==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==3.4.9
|
systembridgeconnector==3.8.2
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
@ -2603,7 +2603,7 @@ twitchAPI==3.10.0
|
|||||||
uasiren==0.0.1
|
uasiren==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.landisgyr_heat_meter
|
# homeassistant.components.landisgyr_heat_meter
|
||||||
ultraheat-api==0.5.1
|
ultraheat-api==0.5.7
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
unifi-discovery==1.1.7
|
unifi-discovery==1.1.7
|
||||||
@ -2781,10 +2781,10 @@ zhong-hong-hvac==1.0.9
|
|||||||
ziggo-mediabox-xl==1.1.0
|
ziggo-mediabox-xl==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-deconz==0.21.0
|
zigpy-deconz==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-xbee==0.18.1
|
zigpy-xbee==0.18.2
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-zigate==0.11.0
|
zigpy-zigate==0.11.0
|
||||||
@ -2799,7 +2799,7 @@ zigpy==0.57.1
|
|||||||
zm-py==0.5.2
|
zm-py==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# homeassistant.components.zwave_js
|
||||||
zwave-js-server-python==0.51.1
|
zwave-js-server-python==0.51.2
|
||||||
|
|
||||||
# homeassistant.components.zwave_me
|
# homeassistant.components.zwave_me
|
||||||
zwave-me-ws==0.4.3
|
zwave-me-ws==0.4.3
|
||||||
|
@ -28,7 +28,7 @@ DoorBirdPy==2.1.0
|
|||||||
HAP-python==4.7.1
|
HAP-python==4.7.1
|
||||||
|
|
||||||
# homeassistant.components.tasmota
|
# homeassistant.components.tasmota
|
||||||
HATasmota==0.7.1
|
HATasmota==0.7.3
|
||||||
|
|
||||||
# homeassistant.components.doods
|
# homeassistant.components.doods
|
||||||
# homeassistant.components.generic
|
# homeassistant.components.generic
|
||||||
@ -108,7 +108,7 @@ PyXiaomiGateway==0.14.3
|
|||||||
RachioPy==1.0.3
|
RachioPy==1.0.3
|
||||||
|
|
||||||
# homeassistant.components.python_script
|
# homeassistant.components.python_script
|
||||||
RestrictedPython==6.1
|
RestrictedPython==6.2
|
||||||
|
|
||||||
# homeassistant.components.remember_the_milk
|
# homeassistant.components.remember_the_milk
|
||||||
RtmAPI==0.7.2
|
RtmAPI==0.7.2
|
||||||
@ -338,13 +338,13 @@ aiosyncthing==0.5.1
|
|||||||
aiotractive==0.5.6
|
aiotractive==0.5.6
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==61
|
aiounifi==62
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.vodafone_station
|
# homeassistant.components.vodafone_station
|
||||||
aiovodafone==0.1.0
|
aiovodafone==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.watttime
|
# homeassistant.components.watttime
|
||||||
aiowatttime==0.1.1
|
aiowatttime==0.1.1
|
||||||
@ -439,7 +439,7 @@ bimmer-connected==0.14.0
|
|||||||
bleak-retry-connector==3.1.3
|
bleak-retry-connector==3.1.3
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak==0.21.0
|
bleak==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox-uniapi==2.1.4
|
blebox-uniapi==2.1.4
|
||||||
@ -454,7 +454,7 @@ bluemaestro-ble==0.2.3
|
|||||||
bluetooth-adapters==0.16.1
|
bluetooth-adapters==0.16.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==1.2.2
|
bluetooth-auto-recovery==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
@ -753,7 +753,7 @@ ha-philipsjs==3.1.0
|
|||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.70.0
|
hass-nabucasa==0.71.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
hassil==1.2.5
|
hassil==1.2.5
|
||||||
@ -777,7 +777,7 @@ hole==0.8.0
|
|||||||
holidays==0.28
|
holidays==0.28
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20230908.0
|
home-assistant-frontend==20230911.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2023.8.2
|
home-assistant-intents==2023.8.2
|
||||||
@ -1086,7 +1086,7 @@ plexauth==0.0.6
|
|||||||
plexwebsocket==0.0.13
|
plexwebsocket==0.0.13
|
||||||
|
|
||||||
# homeassistant.components.plugwise
|
# homeassistant.components.plugwise
|
||||||
plugwise==0.31.9
|
plugwise==0.32.2
|
||||||
|
|
||||||
# homeassistant.components.plum_lightpad
|
# homeassistant.components.plum_lightpad
|
||||||
plumlightpad==0.0.11
|
plumlightpad==0.0.11
|
||||||
@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6
|
|||||||
pymochad==0.2.0
|
pymochad==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
# homeassistant.components.modbus
|
||||||
pymodbus==3.5.1
|
pymodbus==3.5.2
|
||||||
|
|
||||||
# homeassistant.components.monoprice
|
# homeassistant.components.monoprice
|
||||||
pymonoprice==0.4
|
pymonoprice==0.4
|
||||||
@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0
|
|||||||
python-qbittorrent==0.4.3
|
python-qbittorrent==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.roborock
|
# homeassistant.components.roborock
|
||||||
python-roborock==0.32.3
|
python-roborock==0.33.2
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.33
|
python-smarttub==0.0.33
|
||||||
@ -1639,7 +1639,7 @@ pyvizio==0.1.61
|
|||||||
pyvolumio==0.1.5
|
pyvolumio==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.waze_travel_time
|
# homeassistant.components.waze_travel_time
|
||||||
pywaze==0.3.0
|
pywaze==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
@ -1837,7 +1837,7 @@ sunwatcher==0.2.1
|
|||||||
surepy==0.8.0
|
surepy==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==3.4.9
|
systembridgeconnector==3.8.2
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
@ -1903,7 +1903,7 @@ twitchAPI==3.10.0
|
|||||||
uasiren==0.0.1
|
uasiren==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.landisgyr_heat_meter
|
# homeassistant.components.landisgyr_heat_meter
|
||||||
ultraheat-api==0.5.1
|
ultraheat-api==0.5.7
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
unifi-discovery==1.1.7
|
unifi-discovery==1.1.7
|
||||||
@ -2045,10 +2045,10 @@ zeversolar==0.3.1
|
|||||||
zha-quirks==0.0.103
|
zha-quirks==0.0.103
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-deconz==0.21.0
|
zigpy-deconz==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-xbee==0.18.1
|
zigpy-xbee==0.18.2
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy-zigate==0.11.0
|
zigpy-zigate==0.11.0
|
||||||
@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4
|
|||||||
zigpy==0.57.1
|
zigpy==0.57.1
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# homeassistant.components.zwave_js
|
||||||
zwave-js-server-python==0.51.1
|
zwave-js-server-python==0.51.2
|
||||||
|
|
||||||
# homeassistant.components.zwave_me
|
# homeassistant.components.zwave_me
|
||||||
zwave-me-ws==0.4.3
|
zwave-me-ws==0.4.3
|
||||||
|
@ -5,8 +5,11 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||||
|
|
||||||
|
from homeassistant.components.airthings_ble.const import DOMAIN
|
||||||
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, MockEntity
|
||||||
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
|
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
|
||||||
|
|
||||||
|
|
||||||
@ -36,18 +39,52 @@ def patch_airthings_ble(return_value=AirthingsDevice, side_effect=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_airthings_device_update():
|
||||||
|
"""Patch airthings-ble device."""
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.airthings_ble.AirthingsBluetoothDeviceData.update_device",
|
||||||
|
return_value=WAVE_DEVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
|
WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
name="cc-cc-cc-cc-cc-cc",
|
name="cc-cc-cc-cc-cc-cc",
|
||||||
address="cc:cc:cc:cc:cc:cc",
|
address="cc:cc:cc:cc:cc:cc",
|
||||||
|
device=generate_ble_device(
|
||||||
|
address="cc:cc:cc:cc:cc:cc",
|
||||||
|
name="Airthings Wave+",
|
||||||
|
),
|
||||||
rssi=-61,
|
rssi=-61,
|
||||||
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
|
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
|
||||||
service_data={},
|
service_data={
|
||||||
service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
|
# Sensor data
|
||||||
source="local",
|
"b42e2a68-ade7-11e4-89d3-123b93f75cba": bytearray(
|
||||||
device=generate_ble_device(
|
b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A"
|
||||||
"cc:cc:cc:cc:cc:cc",
|
|
||||||
"cc-cc-cc-cc-cc-cc",
|
|
||||||
),
|
),
|
||||||
|
# Manufacturer
|
||||||
|
"00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"),
|
||||||
|
# Model
|
||||||
|
"00002a24-0000-1000-8000-00805f9b34fb": bytearray(b"2930"),
|
||||||
|
# Identifier
|
||||||
|
"00002a25-0000-1000-8000-00805f9b34fb": bytearray(b"123456"),
|
||||||
|
# SW Version
|
||||||
|
"00002a26-0000-1000-8000-00805f9b34fb": bytearray(b"G-BLE-1.5.3-master+0"),
|
||||||
|
# HW Version
|
||||||
|
"00002a27-0000-1000-8000-00805f9b34fb": bytearray(b"REV A"),
|
||||||
|
# Command
|
||||||
|
"b42e2d06-ade7-11e4-89d3-123b93f75cba": bytearray(b"\x00"),
|
||||||
|
},
|
||||||
|
service_uuids=[
|
||||||
|
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
|
||||||
|
"b42e2a68-ade7-11e4-89d3-123b93f75cba",
|
||||||
|
"00002a29-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a24-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a25-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a26-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a27-0000-1000-8000-00805f9b34fb",
|
||||||
|
"b42e2d06-ade7-11e4-89d3-123b93f75cba",
|
||||||
|
],
|
||||||
|
source="local",
|
||||||
advertisement=generate_advertisement_data(
|
advertisement=generate_advertisement_data(
|
||||||
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
|
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
|
||||||
service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
|
service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
|
||||||
@ -99,3 +136,62 @@ WAVE_DEVICE_INFO = AirthingsDevice(
|
|||||||
},
|
},
|
||||||
address="cc:cc:cc:cc:cc:cc",
|
address="cc:cc:cc:cc:cc:cc",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TEMPERATURE_V1 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus 123456_temperature",
|
||||||
|
name="Airthings Wave Plus 123456 Temperature",
|
||||||
|
)
|
||||||
|
|
||||||
|
HUMIDITY_V2 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus (123456)_humidity",
|
||||||
|
name="Airthings Wave Plus (123456) Humidity",
|
||||||
|
)
|
||||||
|
|
||||||
|
CO2_V1 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus 123456_co2",
|
||||||
|
name="Airthings Wave Plus 123456 CO2",
|
||||||
|
)
|
||||||
|
|
||||||
|
CO2_V2 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus (123456)_co2",
|
||||||
|
name="Airthings Wave Plus (123456) CO2",
|
||||||
|
)
|
||||||
|
|
||||||
|
VOC_V1 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus 123456_voc",
|
||||||
|
name="Airthings Wave Plus 123456 CO2",
|
||||||
|
)
|
||||||
|
|
||||||
|
VOC_V2 = MockEntity(
|
||||||
|
unique_id="Airthings Wave Plus (123456)_voc",
|
||||||
|
name="Airthings Wave Plus (123456) VOC",
|
||||||
|
)
|
||||||
|
|
||||||
|
VOC_V3 = MockEntity(
|
||||||
|
unique_id="cc:cc:cc:cc:cc:cc_voc",
|
||||||
|
name="Airthings Wave Plus (123456) VOC",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_entry(hass):
|
||||||
|
"""Create a config entry."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=WAVE_SERVICE_INFO.address,
|
||||||
|
title="Airthings Wave Plus (123456)",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def create_device(hass, entry):
|
||||||
|
"""Create a device for the given entry."""
|
||||||
|
device_registry = hass.helpers.device_registry.async_get(hass)
|
||||||
|
device = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)},
|
||||||
|
manufacturer="Airthings AS",
|
||||||
|
name="Airthings Wave Plus (123456)",
|
||||||
|
model="Wave Plus",
|
||||||
|
)
|
||||||
|
return device
|
||||||
|
213
tests/components/airthings_ble/test_sensor.py
Normal file
213
tests/components/airthings_ble/test_sensor.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
"""Test the Airthings Wave sensor."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.airthings_ble.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.components.airthings_ble import (
|
||||||
|
CO2_V1,
|
||||||
|
CO2_V2,
|
||||||
|
HUMIDITY_V2,
|
||||||
|
TEMPERATURE_V1,
|
||||||
|
VOC_V1,
|
||||||
|
VOC_V2,
|
||||||
|
VOC_V3,
|
||||||
|
WAVE_DEVICE_INFO,
|
||||||
|
WAVE_SERVICE_INFO,
|
||||||
|
create_device,
|
||||||
|
create_entry,
|
||||||
|
patch_airthings_device_update,
|
||||||
|
)
|
||||||
|
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant):
|
||||||
|
"""Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format."""
|
||||||
|
entry = create_entry(hass)
|
||||||
|
device = create_device(hass, entry)
|
||||||
|
|
||||||
|
assert entry is not None
|
||||||
|
assert device is not None
|
||||||
|
|
||||||
|
entity_registry = hass.helpers.entity_registry.async_get(hass)
|
||||||
|
|
||||||
|
sensor = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=TEMPERATURE_V1.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
inject_bluetooth_service_info(
|
||||||
|
hass,
|
||||||
|
WAVE_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch_airthings_device_update():
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) > 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
entity_registry.async_get(sensor.entity_id).unique_id
|
||||||
|
== WAVE_DEVICE_INFO.address + "_temperature"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||||
|
"""Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format."""
|
||||||
|
entry = create_entry(hass)
|
||||||
|
device = create_device(hass, entry)
|
||||||
|
|
||||||
|
assert entry is not None
|
||||||
|
assert device is not None
|
||||||
|
|
||||||
|
entity_registry = hass.helpers.entity_registry.async_get(hass)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
sensor = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=HUMIDITY_V2.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
inject_bluetooth_service_info(
|
||||||
|
hass,
|
||||||
|
WAVE_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch_airthings_device_update():
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) > 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
entity_registry.async_get(sensor.entity_id).unique_id
|
||||||
|
== WAVE_DEVICE_INFO.address + "_humidity"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||||
|
"""Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids."""
|
||||||
|
entry = create_entry(hass)
|
||||||
|
device = create_device(hass, entry)
|
||||||
|
|
||||||
|
assert entry is not None
|
||||||
|
assert device is not None
|
||||||
|
|
||||||
|
entity_registry = hass.helpers.entity_registry.async_get(hass)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
v2 = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=CO2_V2.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
v1 = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=CO2_V1.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
inject_bluetooth_service_info(
|
||||||
|
hass,
|
||||||
|
WAVE_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch_airthings_device_update():
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) > 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
entity_registry.async_get(v1.entity_id).unique_id
|
||||||
|
== WAVE_DEVICE_INFO.address + "_co2"
|
||||||
|
)
|
||||||
|
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||||
|
"""Test if migration works when we have all unique ids."""
|
||||||
|
entry = create_entry(hass)
|
||||||
|
device = create_device(hass, entry)
|
||||||
|
|
||||||
|
assert entry is not None
|
||||||
|
assert device is not None
|
||||||
|
|
||||||
|
entity_registry = hass.helpers.entity_registry.async_get(hass)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
v1 = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=VOC_V1.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
v2 = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=VOC_V2.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
v3 = entity_registry.async_get_or_create(
|
||||||
|
domain=DOMAIN,
|
||||||
|
platform="sensor",
|
||||||
|
unique_id=VOC_V3.unique_id,
|
||||||
|
config_entry=entry,
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
inject_bluetooth_service_info(
|
||||||
|
hass,
|
||||||
|
WAVE_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch_airthings_device_update():
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) > 0
|
||||||
|
|
||||||
|
assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id
|
||||||
|
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
|
||||||
|
assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id
|
@ -32,7 +32,6 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
|
|||||||
"relayer_server": "test-relayer-server",
|
"relayer_server": "test-relayer-server",
|
||||||
"accounts_server": "test-acounts-server",
|
"accounts_server": "test-acounts-server",
|
||||||
"cloudhook_server": "test-cloudhook-server",
|
"cloudhook_server": "test-cloudhook-server",
|
||||||
"remote_sni_server": "test-remote-sni-server",
|
|
||||||
"alexa_server": "test-alexa-server",
|
"alexa_server": "test-alexa-server",
|
||||||
"acme_server": "test-acme-server",
|
"acme_server": "test-acme-server",
|
||||||
"remotestate_server": "test-remotestate-server",
|
"remotestate_server": "test-remotestate-server",
|
||||||
|
@ -8,11 +8,25 @@ from homeassistant.config_entries import ConfigEntryState
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .const import MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL, MOCK_USER_DATA
|
from .const import (
|
||||||
|
MOCK_FB_SERVICES,
|
||||||
|
MOCK_FIRMWARE_AVAILABLE,
|
||||||
|
MOCK_FIRMWARE_RELEASE_URL,
|
||||||
|
MOCK_USER_DATA,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
AVAILABLE_UPDATE = {
|
||||||
|
"UserInterface1": {
|
||||||
|
"GetInfo": {
|
||||||
|
"NewX_AVM-DE_Version": MOCK_FIRMWARE_AVAILABLE,
|
||||||
|
"NewX_AVM-DE_InfoURL": MOCK_FIRMWARE_RELEASE_URL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_update_entities_initialized(
|
async def test_update_entities_initialized(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -41,10 +55,8 @@ async def test_update_available(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test update entities."""
|
"""Test update entities."""
|
||||||
|
|
||||||
with patch(
|
fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE})
|
||||||
"homeassistant.components.fritz.common.FritzBoxTools._update_device_info",
|
|
||||||
return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL),
|
|
||||||
):
|
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
@ -90,10 +102,9 @@ async def test_available_update_can_be_installed(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test update entities."""
|
"""Test update entities."""
|
||||||
|
|
||||||
|
fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE})
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.fritz.common.FritzBoxTools._update_device_info",
|
|
||||||
return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL),
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.fritz.common.FritzBoxTools.async_trigger_firmware_update",
|
"homeassistant.components.fritz.common.FritzBoxTools.async_trigger_firmware_update",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mocked_update_call:
|
) as mocked_update_call:
|
||||||
|
@ -633,6 +633,41 @@ async def test_invalid_service_calls(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_addon_service_call_with_complex_slug(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
) -> None:
|
||||||
|
"""Addon slugs can have ., - and _, confirm that passes validation."""
|
||||||
|
supervisor_mock_data = {
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"auto_update": True,
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "test.a_1-2",
|
||||||
|
"slug": "test.a_1-2",
|
||||||
|
"state": "stopped",
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"repository": "core",
|
||||||
|
"icon": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.is_connected",
|
||||||
|
return_value=None,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
||||||
|
return_value=supervisor_mock_data,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, "hassio", {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call("hassio", "addon_start", {"addon": "test.a_1-2"})
|
||||||
|
|
||||||
|
|
||||||
async def test_service_calls_core(
|
async def test_service_calls_core(
|
||||||
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
|
|||||||
with patch(
|
with patch(
|
||||||
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
|
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
|
||||||
return_value=mock_connect_app,
|
return_value=mock_connect_app,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.async_setup_entry",
|
"homeassistant.components.zha.async_setup_entry",
|
||||||
|
@ -25,7 +25,7 @@ def mock_zha():
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
|
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
|
||||||
return_value=mock_connect_app,
|
return_value=mock_connect_app,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.async_setup_entry",
|
"homeassistant.components.zha.async_setup_entry",
|
||||||
|
@ -45,7 +45,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
|
|||||||
with patch(
|
with patch(
|
||||||
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
|
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
|
||||||
return_value=mock_connect_app,
|
return_value=mock_connect_app,
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
|
|||||||
with patch(
|
with patch(
|
||||||
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
|
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
|
||||||
return_value=mock_connect_app,
|
return_value=mock_connect_app,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zha.async_setup_entry",
|
"homeassistant.components.zha.async_setup_entry",
|
||||||
|
35
tests/components/ipp/fixtures/printer_without_uuid.json
Normal file
35
tests/components/ipp/fixtures/printer_without_uuid.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"printer-state": "idle",
|
||||||
|
"printer-name": "Test Printer",
|
||||||
|
"printer-location": null,
|
||||||
|
"printer-make-and-model": "Test HA-1000 Series",
|
||||||
|
"printer-device-id": "MFG:TEST;CMD:ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF;MDL:HA-1000 Series;CLS:PRINTER;DES:TEST HA-1000 Series;CID:EpsonRGB;FID:FXN,DPA,WFA,ETN,AFN,DAN,WRA;RID:20;DDS:022500;ELG:1000;SN:555534593035345555;URF:CP1,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7-6,V1.4,MT1-3-7-8-10-11-12;",
|
||||||
|
"printer-uri-supported": [
|
||||||
|
"ipps://192.168.1.31:631/ipp/print",
|
||||||
|
"ipp://192.168.1.31:631/ipp/print"
|
||||||
|
],
|
||||||
|
"uri-authentication-supported": ["none", "none"],
|
||||||
|
"uri-security-supported": ["tls", "none"],
|
||||||
|
"printer-info": "Test HA-1000 Series",
|
||||||
|
"printer-up-time": 30,
|
||||||
|
"printer-firmware-string-version": "20.23.06HA",
|
||||||
|
"printer-more-info": "http://192.168.1.31:80/PRESENTATION/BONJOUR",
|
||||||
|
"marker-names": [
|
||||||
|
"Black ink",
|
||||||
|
"Photo black ink",
|
||||||
|
"Cyan ink",
|
||||||
|
"Yellow ink",
|
||||||
|
"Magenta ink"
|
||||||
|
],
|
||||||
|
"marker-types": [
|
||||||
|
"ink-cartridge",
|
||||||
|
"ink-cartridge",
|
||||||
|
"ink-cartridge",
|
||||||
|
"ink-cartridge",
|
||||||
|
"ink-cartridge"
|
||||||
|
],
|
||||||
|
"marker-colors": ["#000000", "#000000", "#00FFFF", "#FFFF00", "#FF00FF"],
|
||||||
|
"marker-levels": [58, 98, 91, 95, 73],
|
||||||
|
"marker-low-levels": [10, 10, 10, 10, 10],
|
||||||
|
"marker-high-levels": [100, 100, 100, 100, 100]
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
"""Tests for the IPP config flow."""
|
"""Tests for the IPP config flow."""
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from unittest.mock import MagicMock
|
import json
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from pyipp import (
|
from pyipp import (
|
||||||
IPPConnectionError,
|
IPPConnectionError,
|
||||||
@ -8,6 +9,7 @@ from pyipp import (
|
|||||||
IPPError,
|
IPPError,
|
||||||
IPPParseError,
|
IPPParseError,
|
||||||
IPPVersionNotSupportedError,
|
IPPVersionNotSupportedError,
|
||||||
|
Printer,
|
||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ from . import (
|
|||||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
|
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
|
||||||
@ -316,6 +318,31 @@ async def test_zeroconf_with_uuid_device_exists_abort(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_with_uuid_device_exists_abort_new_host(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_ipp_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort zeroconf flow if printer already configured."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO, host="1.2.3.9")
|
||||||
|
discovery_info.properties = {
|
||||||
|
**MOCK_ZEROCONF_IPP_SERVICE_INFO.properties,
|
||||||
|
"UUID": "cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data=discovery_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert mock_config_entry.data[CONF_HOST] == "1.2.3.9"
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_empty_unique_id(
|
async def test_zeroconf_empty_unique_id(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_ipp_config_flow: MagicMock,
|
mock_ipp_config_flow: MagicMock,
|
||||||
@ -337,6 +364,21 @@ async def test_zeroconf_empty_unique_id(
|
|||||||
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "EPSON XP-6000 Series"
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||||
|
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_no_unique_id(
|
async def test_zeroconf_no_unique_id(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -355,6 +397,21 @@ async def test_zeroconf_no_unique_id(
|
|||||||
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "EPSON XP-6000 Series"
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||||
|
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||||
|
|
||||||
|
|
||||||
async def test_full_user_flow_implementation(
|
async def test_full_user_flow_implementation(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -448,3 +505,45 @@ async def test_full_zeroconf_tls_flow_implementation(
|
|||||||
|
|
||||||
assert result["result"]
|
assert result["result"]
|
||||||
assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_empty_unique_id_uses_serial(hass: HomeAssistant) -> None:
|
||||||
|
"""Test zeroconf flow if printer lacks (empty) unique identification with serial fallback."""
|
||||||
|
fixture = await hass.async_add_executor_job(
|
||||||
|
load_fixture, "ipp/printer_without_uuid.json"
|
||||||
|
)
|
||||||
|
mock_printer_without_uuid = Printer.from_dict(json.loads(fixture))
|
||||||
|
mock_printer_without_uuid.unique_id = None
|
||||||
|
|
||||||
|
discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO)
|
||||||
|
discovery_info.properties = {
|
||||||
|
**MOCK_ZEROCONF_IPP_SERVICE_INFO.properties,
|
||||||
|
"UUID": "",
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ipp.config_flow.IPP", autospec=True
|
||||||
|
) as ipp_mock:
|
||||||
|
client = ipp_mock.return_value
|
||||||
|
client.printer.return_value = mock_printer_without_uuid
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data=discovery_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "EPSON XP-6000 Series"
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||||
|
assert result["data"][CONF_UUID] == ""
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == "555534593035345555"
|
||||||
|
@ -149,7 +149,7 @@ async def mock_do_cycle_fixture(
|
|||||||
mock_pymodbus_return,
|
mock_pymodbus_return,
|
||||||
) -> FrozenDateTimeFactory:
|
) -> FrozenDateTimeFactory:
|
||||||
"""Trigger update call with time_changed event."""
|
"""Trigger update call with time_changed event."""
|
||||||
freezer.tick(timedelta(seconds=90))
|
freezer.tick(timedelta(seconds=1))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
return freezer
|
return freezer
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""The tests for the Modbus sensor component."""
|
"""The tests for the Modbus sensor component."""
|
||||||
|
import struct
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -267,7 +269,6 @@ async def test_config_wrong_struct_sensor(
|
|||||||
{
|
{
|
||||||
CONF_NAME: TEST_ENTITY_NAME,
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
CONF_ADDRESS: 51,
|
CONF_ADDRESS: 51,
|
||||||
CONF_SCAN_INTERVAL: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -625,6 +626,21 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("config_addon", "register_words", "do_exception", "expected"),
|
("config_addon", "register_words", "do_exception", "expected"),
|
||||||
[
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_SLAVE_COUNT: 1,
|
||||||
|
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
|
||||||
|
CONF_DATA_TYPE: DataType.FLOAT32,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
0x5102,
|
||||||
|
0x0304,
|
||||||
|
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
|
||||||
|
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
|
||||||
|
],
|
||||||
|
False,
|
||||||
|
["34899771392", "0"],
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
CONF_SLAVE_COUNT: 0,
|
CONF_SLAVE_COUNT: 0,
|
||||||
@ -710,7 +726,6 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non
|
|||||||
CONF_NAME: TEST_ENTITY_NAME,
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
CONF_ADDRESS: 51,
|
CONF_ADDRESS: 51,
|
||||||
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
|
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
|
||||||
CONF_SCAN_INTERVAL: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -902,6 +917,65 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
|
|||||||
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
CONF_SCAN_INTERVAL: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config_addon", "register_words", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_DATA_TYPE: DataType.FLOAT32,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
|
||||||
|
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
|
||||||
|
],
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_DATA_TYPE: DataType.FLOAT32,
|
||||||
|
},
|
||||||
|
[0x6E61, 0x6E00],
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_DATA_TYPE: DataType.CUSTOM,
|
||||||
|
CONF_COUNT: 2,
|
||||||
|
CONF_STRUCTURE: "4s",
|
||||||
|
},
|
||||||
|
[0x6E61, 0x6E00],
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_DATA_TYPE: DataType.CUSTOM,
|
||||||
|
CONF_COUNT: 2,
|
||||||
|
CONF_STRUCTURE: "4s",
|
||||||
|
},
|
||||||
|
[0x6161, 0x6100],
|
||||||
|
"aaa\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_unpack_ok(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
||||||
|
"""Run test for sensor."""
|
||||||
|
assert hass.states.get(ENTITY_ID).state == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"do_config",
|
"do_config",
|
||||||
[
|
[
|
||||||
@ -918,27 +992,23 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("register_words", "do_exception", "start_expect", "end_expect"),
|
("register_words", "do_exception"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
[0x8000],
|
[0x8000],
|
||||||
True,
|
True,
|
||||||
"17",
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_lazy_error_sensor(
|
async def test_lazy_error_sensor(
|
||||||
hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory, start_expect, end_expect
|
hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run test for sensor."""
|
"""Run test for sensor."""
|
||||||
hass.states.async_set(ENTITY_ID, 17)
|
hass.states.async_set(ENTITY_ID, 17)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get(ENTITY_ID).state == start_expect
|
assert hass.states.get(ENTITY_ID).state == "17"
|
||||||
await do_next_cycle(hass, mock_do_cycle, 11)
|
await do_next_cycle(hass, mock_do_cycle, 5)
|
||||||
assert hass.states.get(ENTITY_ID).state == start_expect
|
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
|
||||||
await do_next_cycle(hass, mock_do_cycle, 11)
|
|
||||||
assert hass.states.get(ENTITY_ID).state == end_expect
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -965,10 +1035,35 @@ async def test_lazy_error_sensor(
|
|||||||
CONF_DATA_TYPE: DataType.CUSTOM,
|
CONF_DATA_TYPE: DataType.CUSTOM,
|
||||||
CONF_STRUCTURE: ">4f",
|
CONF_STRUCTURE: ">4f",
|
||||||
},
|
},
|
||||||
# floats: 7.931250095367432, 10.600000381469727,
|
# floats: nan, 10.600000381469727,
|
||||||
# 1.000879611487865e-28, 10.566553115844727
|
# 1.000879611487865e-28, 10.566553115844727
|
||||||
[0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A],
|
[
|
||||||
"7.93,10.60,0.00,10.57",
|
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
|
||||||
|
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
|
||||||
|
0x4129,
|
||||||
|
0x999A,
|
||||||
|
0x10FD,
|
||||||
|
0xC0CD,
|
||||||
|
0x4129,
|
||||||
|
0x109A,
|
||||||
|
],
|
||||||
|
"0,10.60,0.00,10.57",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_COUNT: 4,
|
||||||
|
CONF_DATA_TYPE: DataType.CUSTOM,
|
||||||
|
CONF_STRUCTURE: ">2i",
|
||||||
|
CONF_NAN_VALUE: 0x0000000F,
|
||||||
|
},
|
||||||
|
# int: nan, 10,
|
||||||
|
[
|
||||||
|
0x0000,
|
||||||
|
0x000F,
|
||||||
|
0x0000,
|
||||||
|
0x000A,
|
||||||
|
],
|
||||||
|
"0,10",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@ -988,6 +1083,18 @@ async def test_lazy_error_sensor(
|
|||||||
[0x0101],
|
[0x0101],
|
||||||
"257",
|
"257",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_COUNT: 8,
|
||||||
|
CONF_PRECISION: 2,
|
||||||
|
CONF_DATA_TYPE: DataType.CUSTOM,
|
||||||
|
CONF_STRUCTURE: ">4f",
|
||||||
|
},
|
||||||
|
# floats: 7.931250095367432, 10.600000381469727,
|
||||||
|
# 1.000879611487865e-28, 10.566553115844727
|
||||||
|
[0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A],
|
||||||
|
"7.93,10.60,0.00,10.57",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
||||||
@ -1003,7 +1110,6 @@ async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> No
|
|||||||
{
|
{
|
||||||
CONF_NAME: TEST_ENTITY_NAME,
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
CONF_ADDRESS: 201,
|
CONF_ADDRESS: 201,
|
||||||
CONF_SCAN_INTERVAL: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
from homeassistant.components import mqtt, sensor
|
from homeassistant.components import mqtt, sensor
|
||||||
from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME
|
from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
Platform,
|
Platform,
|
||||||
@ -324,7 +325,6 @@ async def test_default_entity_and_device_name(
|
|||||||
|
|
||||||
This is a test helper for the _setup_common_attributes_from_config mixin.
|
This is a test helper for the _setup_common_attributes_from_config mixin.
|
||||||
"""
|
"""
|
||||||
# mqtt_mock = await mqtt_mock_entry()
|
|
||||||
|
|
||||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||||
hass.state = CoreState.starting
|
hass.state = CoreState.starting
|
||||||
@ -352,3 +352,61 @@ async def test_default_entity_and_device_name(
|
|||||||
|
|
||||||
# Assert that an issues ware registered
|
# Assert that an issues ware registered
|
||||||
assert len(events) == issue_events
|
assert len(events) == issue_events
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR])
|
||||||
|
async def test_name_attribute_is_set_or_not(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test frendly name with device_class set.
|
||||||
|
|
||||||
|
This is a test helper for the _setup_common_attributes_from_config mixin.
|
||||||
|
"""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
"homeassistant/binary_sensor/bla/config",
|
||||||
|
'{ "name": "Gate", "state_topic": "test-topic", "device_class": "door", '
|
||||||
|
'"object_id": "gate",'
|
||||||
|
'"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}'
|
||||||
|
"}",
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.gate")
|
||||||
|
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gate"
|
||||||
|
|
||||||
|
# Remove the name in a discovery update
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
"homeassistant/binary_sensor/bla/config",
|
||||||
|
'{ "state_topic": "test-topic", "device_class": "door", '
|
||||||
|
'"object_id": "gate",'
|
||||||
|
'"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}'
|
||||||
|
"}",
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.gate")
|
||||||
|
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Door"
|
||||||
|
|
||||||
|
# Set the name to `null` in a discovery update
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
"homeassistant/binary_sensor/bla/config",
|
||||||
|
'{ "name": null, "state_topic": "test-topic", "device_class": "door", '
|
||||||
|
'"object_id": "gate",'
|
||||||
|
'"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}'
|
||||||
|
"}",
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.gate")
|
||||||
|
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) is None
|
||||||
|
@ -20,6 +20,12 @@
|
|||||||
"setpoint": 13.0,
|
"setpoint": 13.0,
|
||||||
"temperature": 24.2
|
"temperature": 24.2
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -43,6 +49,12 @@
|
|||||||
"temperature_difference": 2.0,
|
"temperature_difference": 2.0,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A07"
|
"zigbee_mac_address": "ABCD012345670A07"
|
||||||
},
|
},
|
||||||
@ -60,6 +72,12 @@
|
|||||||
"temperature_difference": 1.7,
|
"temperature_difference": 1.7,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A05"
|
"zigbee_mac_address": "ABCD012345670A05"
|
||||||
},
|
},
|
||||||
@ -99,6 +117,12 @@
|
|||||||
"setpoint": 13.0,
|
"setpoint": 13.0,
|
||||||
"temperature": 30.0
|
"temperature": 30.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -122,6 +146,12 @@
|
|||||||
"temperature_difference": 1.8,
|
"temperature_difference": 1.8,
|
||||||
"valve_position": 100
|
"valve_position": 100
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A09"
|
"zigbee_mac_address": "ABCD012345670A09"
|
||||||
},
|
},
|
||||||
@ -145,6 +175,12 @@
|
|||||||
"setpoint": 13.0,
|
"setpoint": 13.0,
|
||||||
"temperature": 30.0
|
"temperature": 30.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -187,6 +223,12 @@
|
|||||||
"temperature_difference": 1.9,
|
"temperature_difference": 1.9,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A04"
|
"zigbee_mac_address": "ABCD012345670A04"
|
||||||
},
|
},
|
||||||
@ -246,6 +288,12 @@
|
|||||||
"setpoint": 9.0,
|
"setpoint": 9.0,
|
||||||
"temperature": 27.4
|
"temperature": 27.4
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 4.0,
|
"lower_bound": 4.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
|
@ -95,6 +95,12 @@
|
|||||||
"temperature_difference": -0.4,
|
"temperature_difference": -0.4,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A17"
|
"zigbee_mac_address": "ABCD012345670A17"
|
||||||
},
|
},
|
||||||
@ -123,6 +129,12 @@
|
|||||||
"setpoint": 15.0,
|
"setpoint": 15.0,
|
||||||
"temperature": 17.2
|
"temperature": 17.2
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -200,6 +212,12 @@
|
|||||||
"temperature_difference": -0.2,
|
"temperature_difference": -0.2,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A09"
|
"zigbee_mac_address": "ABCD012345670A09"
|
||||||
},
|
},
|
||||||
@ -217,6 +235,12 @@
|
|||||||
"temperature_difference": 3.5,
|
"temperature_difference": 3.5,
|
||||||
"valve_position": 100
|
"valve_position": 100
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A02"
|
"zigbee_mac_address": "ABCD012345670A02"
|
||||||
},
|
},
|
||||||
@ -245,6 +269,12 @@
|
|||||||
"setpoint": 21.5,
|
"setpoint": 21.5,
|
||||||
"temperature": 20.9
|
"temperature": 20.9
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -289,6 +319,12 @@
|
|||||||
"temperature_difference": 0.1,
|
"temperature_difference": 0.1,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A10"
|
"zigbee_mac_address": "ABCD012345670A10"
|
||||||
},
|
},
|
||||||
@ -317,6 +353,12 @@
|
|||||||
"setpoint": 13.0,
|
"setpoint": 13.0,
|
||||||
"temperature": 16.5
|
"temperature": 16.5
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -353,6 +395,12 @@
|
|||||||
"temperature_difference": 0.0,
|
"temperature_difference": 0.0,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
@ -387,6 +435,12 @@
|
|||||||
"setpoint": 14.0,
|
"setpoint": 14.0,
|
||||||
"temperature": 18.9
|
"temperature": 18.9
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
|
@ -76,6 +76,12 @@
|
|||||||
"setpoint": 20.5,
|
"setpoint": 20.5,
|
||||||
"temperature": 19.3
|
"temperature": 19.3
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": -0.5,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 4.0,
|
"lower_bound": 4.0,
|
||||||
"resolution": 0.1,
|
"resolution": 0.1,
|
||||||
|
@ -40,6 +40,12 @@
|
|||||||
"temperature_difference": 2.3,
|
"temperature_difference": 2.3,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A01"
|
"zigbee_mac_address": "ABCD012345670A01"
|
||||||
},
|
},
|
||||||
@ -118,6 +124,12 @@
|
|||||||
"setpoint_low": 20.0,
|
"setpoint_low": 20.0,
|
||||||
"temperature": 239
|
"temperature": 239
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
|
@ -45,6 +45,12 @@
|
|||||||
"temperature_difference": 2.3,
|
"temperature_difference": 2.3,
|
||||||
"valve_position": 0.0
|
"valve_position": 0.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.1,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A01"
|
"zigbee_mac_address": "ABCD012345670A01"
|
||||||
},
|
},
|
||||||
@ -114,6 +120,12 @@
|
|||||||
"setpoint": 15.0,
|
"setpoint": 15.0,
|
||||||
"temperature": 17.9
|
"temperature": 17.9
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 0.0,
|
"lower_bound": 0.0,
|
||||||
"resolution": 0.01,
|
"resolution": 0.01,
|
||||||
|
@ -78,6 +78,12 @@
|
|||||||
"setpoint_low": 20.5,
|
"setpoint_low": 20.5,
|
||||||
"temperature": 26.3
|
"temperature": 26.3
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": -0.5,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 4.0,
|
"lower_bound": 4.0,
|
||||||
"resolution": 0.1,
|
"resolution": 0.1,
|
||||||
|
@ -78,6 +78,12 @@
|
|||||||
"setpoint_low": 20.5,
|
"setpoint_low": 20.5,
|
||||||
"temperature": 23.0
|
"temperature": 23.0
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": -0.5,
|
||||||
|
"upper_bound": 2.0
|
||||||
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"lower_bound": 4.0,
|
"lower_bound": 4.0,
|
||||||
"resolution": 0.1,
|
"resolution": 0.1,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"03e65b16e4b247a29ae0d75a78cb492e": {
|
"03e65b16e4b247a29ae0d75a78cb492e": {
|
||||||
"binary_sensors": {
|
"binary_sensors": {
|
||||||
"plugwise_notification": false
|
"plugwise_notification": true
|
||||||
},
|
},
|
||||||
"dev_class": "gateway",
|
"dev_class": "gateway",
|
||||||
"firmware": "4.4.2",
|
"firmware": "4.4.2",
|
||||||
@ -51,7 +51,11 @@
|
|||||||
},
|
},
|
||||||
"gateway": {
|
"gateway": {
|
||||||
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
|
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
|
||||||
"notifications": {},
|
"notifications": {
|
||||||
|
"97a04c0c263049b29350a660b4cdd01e": {
|
||||||
|
"warning": "The Smile P1 is not connected to a smart meter."
|
||||||
|
}
|
||||||
|
},
|
||||||
"smile_name": "Smile P1"
|
"smile_name": "Smile P1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,5 @@
|
|||||||
{}
|
{
|
||||||
|
"97a04c0c263049b29350a660b4cdd01e": {
|
||||||
|
"warning": "The Smile P1 is not connected to a smart meter."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -48,15 +48,6 @@
|
|||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"zigbee_mac_address": "ABCD012345670A07"
|
"zigbee_mac_address": "ABCD012345670A07"
|
||||||
},
|
},
|
||||||
"71e1944f2a944b26ad73323e399efef0": {
|
|
||||||
"dev_class": "switching",
|
|
||||||
"members": ["5ca521ac179d468e91d772eeeb8a2117"],
|
|
||||||
"model": "Switchgroup",
|
|
||||||
"name": "Test",
|
|
||||||
"switches": {
|
|
||||||
"relay": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aac7b735042c4832ac9ff33aae4f453b": {
|
"aac7b735042c4832ac9ff33aae4f453b": {
|
||||||
"dev_class": "dishwasher",
|
"dev_class": "dishwasher",
|
||||||
"firmware": "2011-06-27T10:52:18+02:00",
|
"firmware": "2011-06-27T10:52:18+02:00",
|
||||||
|
@ -31,159 +31,141 @@ async def test_diagnostics(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"devices": {
|
"devices": {
|
||||||
"df4a4a8169904cdb9c03d61a21f42140": {
|
"02cf28bfec924855854c544690a609ef": {
|
||||||
"dev_class": "zone_thermostat",
|
|
||||||
"firmware": "2016-10-27T02:00:00+02:00",
|
|
||||||
"hardware": "255",
|
|
||||||
"location": "12493538af164a409c6a1c79e38afe1c",
|
|
||||||
"model": "Lisa",
|
|
||||||
"name": "Zone Lisa Bios",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A06",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"thermostat": {
|
|
||||||
"setpoint": 13.0,
|
|
||||||
"lower_bound": 0.0,
|
|
||||||
"upper_bound": 99.9,
|
|
||||||
"resolution": 0.01,
|
|
||||||
},
|
|
||||||
"available": True,
|
|
||||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
|
||||||
"active_preset": "away",
|
|
||||||
"available_schedules": [
|
|
||||||
"CV Roan",
|
|
||||||
"Bios Schema met Film Avond",
|
|
||||||
"GF7 Woonkamer",
|
|
||||||
"Badkamer Schema",
|
|
||||||
"CV Jessie",
|
|
||||||
],
|
|
||||||
"select_schedule": "None",
|
|
||||||
"last_used": "Badkamer Schema",
|
|
||||||
"mode": "heat",
|
|
||||||
"sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67},
|
|
||||||
},
|
|
||||||
"b310b72a0e354bfab43089919b9a88bf": {
|
|
||||||
"dev_class": "thermo_sensor",
|
|
||||||
"firmware": "2019-03-27T01:00:00+01:00",
|
|
||||||
"hardware": "1",
|
|
||||||
"location": "c50f167537524366a5af7aa3942feb1e",
|
|
||||||
"model": "Tom/Floor",
|
|
||||||
"name": "Floor kraan",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A02",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
"available": True,
|
||||||
|
"dev_class": "vcr",
|
||||||
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
|
"model": "Plug",
|
||||||
|
"name": "NVR",
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"temperature": 26.0,
|
"electricity_consumed": 34.0,
|
||||||
"setpoint": 21.5,
|
"electricity_consumed_interval": 9.15,
|
||||||
"temperature_difference": 3.5,
|
"electricity_produced": 0.0,
|
||||||
"valve_position": 100,
|
"electricity_produced_interval": 0.0,
|
||||||
},
|
},
|
||||||
},
|
"switches": {"lock": True, "relay": True},
|
||||||
"a2c3583e0a6349358998b760cea82d2a": {
|
|
||||||
"dev_class": "thermo_sensor",
|
|
||||||
"firmware": "2019-03-27T01:00:00+01:00",
|
|
||||||
"hardware": "1",
|
|
||||||
"location": "12493538af164a409c6a1c79e38afe1c",
|
|
||||||
"model": "Tom/Floor",
|
|
||||||
"name": "Bios Cv Thermostatic Radiator ",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A09",
|
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"available": True,
|
"zigbee_mac_address": "ABCD012345670A15",
|
||||||
"sensors": {
|
|
||||||
"temperature": 17.2,
|
|
||||||
"setpoint": 13.0,
|
|
||||||
"battery": 62,
|
|
||||||
"temperature_difference": -0.2,
|
|
||||||
"valve_position": 0.0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"b59bcebaf94b499ea7d46e4a66fb62d8": {
|
|
||||||
"dev_class": "zone_thermostat",
|
|
||||||
"firmware": "2016-08-02T02:00:00+02:00",
|
|
||||||
"hardware": "255",
|
|
||||||
"location": "c50f167537524366a5af7aa3942feb1e",
|
|
||||||
"model": "Lisa",
|
|
||||||
"name": "Zone Lisa WK",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A07",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"thermostat": {
|
|
||||||
"setpoint": 21.5,
|
|
||||||
"lower_bound": 0.0,
|
|
||||||
"upper_bound": 99.9,
|
|
||||||
"resolution": 0.01,
|
|
||||||
},
|
|
||||||
"available": True,
|
|
||||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
|
||||||
"active_preset": "home",
|
|
||||||
"available_schedules": [
|
|
||||||
"CV Roan",
|
|
||||||
"Bios Schema met Film Avond",
|
|
||||||
"GF7 Woonkamer",
|
|
||||||
"Badkamer Schema",
|
|
||||||
"CV Jessie",
|
|
||||||
],
|
|
||||||
"select_schedule": "GF7 Woonkamer",
|
|
||||||
"last_used": "GF7 Woonkamer",
|
|
||||||
"mode": "auto",
|
|
||||||
"sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34},
|
|
||||||
},
|
|
||||||
"fe799307f1624099878210aa0b9f1475": {
|
|
||||||
"dev_class": "gateway",
|
|
||||||
"firmware": "3.0.15",
|
|
||||||
"hardware": "AME Smile 2.0 board",
|
|
||||||
"location": "1f9dcf83fd4e4b66b72ff787957bfe5d",
|
|
||||||
"mac_address": "012345670001",
|
|
||||||
"model": "Gateway",
|
|
||||||
"name": "Adam",
|
|
||||||
"zigbee_mac_address": "ABCD012345670101",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"select_regulation_mode": "heating",
|
|
||||||
"binary_sensors": {"plugwise_notification": True},
|
|
||||||
"sensors": {"outdoor_temperature": 7.81},
|
|
||||||
},
|
|
||||||
"d3da73bde12a47d5a6b8f9dad971f2ec": {
|
|
||||||
"dev_class": "thermo_sensor",
|
|
||||||
"firmware": "2019-03-27T01:00:00+01:00",
|
|
||||||
"hardware": "1",
|
|
||||||
"location": "82fa13f017d240daa0d0ea1775420f24",
|
|
||||||
"model": "Tom/Floor",
|
|
||||||
"name": "Thermostatic Radiator Jessie",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A10",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
|
||||||
"temperature": 17.1,
|
|
||||||
"setpoint": 15.0,
|
|
||||||
"battery": 62,
|
|
||||||
"temperature_difference": 0.1,
|
|
||||||
"valve_position": 0.0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"21f2b542c49845e6bb416884c55778d6": {
|
"21f2b542c49845e6bb416884c55778d6": {
|
||||||
|
"available": True,
|
||||||
"dev_class": "game_console",
|
"dev_class": "game_console",
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
"model": "Plug",
|
"model": "Plug",
|
||||||
"name": "Playstation Smart Plug",
|
"name": "Playstation Smart Plug",
|
||||||
"zigbee_mac_address": "ABCD012345670A12",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"electricity_consumed": 82.6,
|
"electricity_consumed": 82.6,
|
||||||
"electricity_consumed_interval": 8.6,
|
"electricity_consumed_interval": 8.6,
|
||||||
"electricity_produced": 0.0,
|
"electricity_produced": 0.0,
|
||||||
"electricity_produced_interval": 0.0,
|
"electricity_produced_interval": 0.0,
|
||||||
},
|
},
|
||||||
"switches": {"relay": True, "lock": False},
|
"switches": {"lock": False, "relay": True},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A12",
|
||||||
|
},
|
||||||
|
"4a810418d5394b3f82727340b91ba740": {
|
||||||
|
"available": True,
|
||||||
|
"dev_class": "router",
|
||||||
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
|
"model": "Plug",
|
||||||
|
"name": "USG Smart Plug",
|
||||||
|
"sensors": {
|
||||||
|
"electricity_consumed": 8.5,
|
||||||
|
"electricity_consumed_interval": 0.0,
|
||||||
|
"electricity_produced": 0.0,
|
||||||
|
"electricity_produced_interval": 0.0,
|
||||||
|
},
|
||||||
|
"switches": {"lock": True, "relay": True},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A16",
|
||||||
|
},
|
||||||
|
"675416a629f343c495449970e2ca37b5": {
|
||||||
|
"available": True,
|
||||||
|
"dev_class": "router",
|
||||||
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
|
"model": "Plug",
|
||||||
|
"name": "Ziggo Modem",
|
||||||
|
"sensors": {
|
||||||
|
"electricity_consumed": 12.2,
|
||||||
|
"electricity_consumed_interval": 2.97,
|
||||||
|
"electricity_produced": 0.0,
|
||||||
|
"electricity_produced_interval": 0.0,
|
||||||
|
},
|
||||||
|
"switches": {"lock": True, "relay": True},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A01",
|
||||||
|
},
|
||||||
|
"680423ff840043738f42cc7f1ff97a36": {
|
||||||
|
"available": True,
|
||||||
|
"dev_class": "thermo_sensor",
|
||||||
|
"firmware": "2019-03-27T01:00:00+01:00",
|
||||||
|
"hardware": "1",
|
||||||
|
"location": "08963fec7c53423ca5680aa4cb502c63",
|
||||||
|
"model": "Tom/Floor",
|
||||||
|
"name": "Thermostatic Radiator Badkamer",
|
||||||
|
"sensors": {
|
||||||
|
"battery": 51,
|
||||||
|
"setpoint": 14.0,
|
||||||
|
"temperature": 19.1,
|
||||||
|
"temperature_difference": -0.4,
|
||||||
|
"valve_position": 0.0,
|
||||||
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A17",
|
||||||
|
},
|
||||||
|
"6a3bf693d05e48e0b460c815a4fdd09d": {
|
||||||
|
"active_preset": "asleep",
|
||||||
|
"available": True,
|
||||||
|
"available_schedules": [
|
||||||
|
"CV Roan",
|
||||||
|
"Bios Schema met Film Avond",
|
||||||
|
"GF7 Woonkamer",
|
||||||
|
"Badkamer Schema",
|
||||||
|
"CV Jessie",
|
||||||
|
],
|
||||||
|
"dev_class": "zone_thermostat",
|
||||||
|
"firmware": "2016-10-27T02:00:00+02:00",
|
||||||
|
"hardware": "255",
|
||||||
|
"last_used": "CV Jessie",
|
||||||
|
"location": "82fa13f017d240daa0d0ea1775420f24",
|
||||||
|
"mode": "auto",
|
||||||
|
"model": "Lisa",
|
||||||
|
"name": "Zone Thermostat Jessie",
|
||||||
|
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||||
|
"select_schedule": "CV Jessie",
|
||||||
|
"sensors": {"battery": 37, "setpoint": 15.0, "temperature": 17.2},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"thermostat": {
|
||||||
|
"lower_bound": 0.0,
|
||||||
|
"resolution": 0.01,
|
||||||
|
"setpoint": 15.0,
|
||||||
|
"upper_bound": 99.9,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A03",
|
||||||
},
|
},
|
||||||
"78d1126fc4c743db81b61c20e88342a7": {
|
"78d1126fc4c743db81b61c20e88342a7": {
|
||||||
|
"available": True,
|
||||||
"dev_class": "central_heating_pump",
|
"dev_class": "central_heating_pump",
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
"location": "c50f167537524366a5af7aa3942feb1e",
|
"location": "c50f167537524366a5af7aa3942feb1e",
|
||||||
"model": "Plug",
|
"model": "Plug",
|
||||||
"name": "CV Pomp",
|
"name": "CV Pomp",
|
||||||
"zigbee_mac_address": "ABCD012345670A05",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"electricity_consumed": 35.6,
|
"electricity_consumed": 35.6,
|
||||||
"electricity_consumed_interval": 7.37,
|
"electricity_consumed_interval": 7.37,
|
||||||
@ -191,153 +173,88 @@ async def test_diagnostics(
|
|||||||
"electricity_produced_interval": 0.0,
|
"electricity_produced_interval": 0.0,
|
||||||
},
|
},
|
||||||
"switches": {"relay": True},
|
"switches": {"relay": True},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A05",
|
||||||
},
|
},
|
||||||
"90986d591dcd426cae3ec3e8111ff730": {
|
"90986d591dcd426cae3ec3e8111ff730": {
|
||||||
|
"binary_sensors": {"heating_state": True},
|
||||||
"dev_class": "heater_central",
|
"dev_class": "heater_central",
|
||||||
"location": "1f9dcf83fd4e4b66b72ff787957bfe5d",
|
"location": "1f9dcf83fd4e4b66b72ff787957bfe5d",
|
||||||
"model": "Unknown",
|
"model": "Unknown",
|
||||||
"name": "OnOff",
|
"name": "OnOff",
|
||||||
"binary_sensors": {"heating_state": True},
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"water_temperature": 70.0,
|
|
||||||
"intended_boiler_temperature": 70.0,
|
"intended_boiler_temperature": 70.0,
|
||||||
"modulation_level": 1,
|
"modulation_level": 1,
|
||||||
|
"water_temperature": 70.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"cd0ddb54ef694e11ac18ed1cbce5dbbd": {
|
|
||||||
"dev_class": "vcr",
|
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
|
||||||
"model": "Plug",
|
|
||||||
"name": "NAS",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A14",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
|
||||||
"electricity_consumed": 16.5,
|
|
||||||
"electricity_consumed_interval": 0.5,
|
|
||||||
"electricity_produced": 0.0,
|
|
||||||
"electricity_produced_interval": 0.0,
|
|
||||||
},
|
|
||||||
"switches": {"relay": True, "lock": True},
|
|
||||||
},
|
|
||||||
"4a810418d5394b3f82727340b91ba740": {
|
|
||||||
"dev_class": "router",
|
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
|
||||||
"model": "Plug",
|
|
||||||
"name": "USG Smart Plug",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A16",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
|
||||||
"electricity_consumed": 8.5,
|
|
||||||
"electricity_consumed_interval": 0.0,
|
|
||||||
"electricity_produced": 0.0,
|
|
||||||
"electricity_produced_interval": 0.0,
|
|
||||||
},
|
|
||||||
"switches": {"relay": True, "lock": True},
|
|
||||||
},
|
|
||||||
"02cf28bfec924855854c544690a609ef": {
|
|
||||||
"dev_class": "vcr",
|
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
|
||||||
"model": "Plug",
|
|
||||||
"name": "NVR",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A15",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
|
||||||
"electricity_consumed": 34.0,
|
|
||||||
"electricity_consumed_interval": 9.15,
|
|
||||||
"electricity_produced": 0.0,
|
|
||||||
"electricity_produced_interval": 0.0,
|
|
||||||
},
|
|
||||||
"switches": {"relay": True, "lock": True},
|
|
||||||
},
|
|
||||||
"a28f588dc4a049a483fd03a30361ad3a": {
|
"a28f588dc4a049a483fd03a30361ad3a": {
|
||||||
|
"available": True,
|
||||||
"dev_class": "settop",
|
"dev_class": "settop",
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
"model": "Plug",
|
"model": "Plug",
|
||||||
"name": "Fibaro HC2",
|
"name": "Fibaro HC2",
|
||||||
"zigbee_mac_address": "ABCD012345670A13",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"electricity_consumed": 12.5,
|
"electricity_consumed": 12.5,
|
||||||
"electricity_consumed_interval": 3.8,
|
"electricity_consumed_interval": 3.8,
|
||||||
"electricity_produced": 0.0,
|
"electricity_produced": 0.0,
|
||||||
"electricity_produced_interval": 0.0,
|
"electricity_produced_interval": 0.0,
|
||||||
},
|
},
|
||||||
"switches": {"relay": True, "lock": True},
|
"switches": {"lock": True, "relay": True},
|
||||||
},
|
|
||||||
"6a3bf693d05e48e0b460c815a4fdd09d": {
|
|
||||||
"dev_class": "zone_thermostat",
|
|
||||||
"firmware": "2016-10-27T02:00:00+02:00",
|
|
||||||
"hardware": "255",
|
|
||||||
"location": "82fa13f017d240daa0d0ea1775420f24",
|
|
||||||
"model": "Lisa",
|
|
||||||
"name": "Zone Thermostat Jessie",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A03",
|
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"thermostat": {
|
"zigbee_mac_address": "ABCD012345670A13",
|
||||||
"setpoint": 15.0,
|
|
||||||
"lower_bound": 0.0,
|
|
||||||
"upper_bound": 99.9,
|
|
||||||
"resolution": 0.01,
|
|
||||||
},
|
},
|
||||||
|
"a2c3583e0a6349358998b760cea82d2a": {
|
||||||
"available": True,
|
"available": True,
|
||||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
|
||||||
"active_preset": "asleep",
|
|
||||||
"available_schedules": [
|
|
||||||
"CV Roan",
|
|
||||||
"Bios Schema met Film Avond",
|
|
||||||
"GF7 Woonkamer",
|
|
||||||
"Badkamer Schema",
|
|
||||||
"CV Jessie",
|
|
||||||
],
|
|
||||||
"select_schedule": "CV Jessie",
|
|
||||||
"last_used": "CV Jessie",
|
|
||||||
"mode": "auto",
|
|
||||||
"sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37},
|
|
||||||
},
|
|
||||||
"680423ff840043738f42cc7f1ff97a36": {
|
|
||||||
"dev_class": "thermo_sensor",
|
"dev_class": "thermo_sensor",
|
||||||
"firmware": "2019-03-27T01:00:00+01:00",
|
"firmware": "2019-03-27T01:00:00+01:00",
|
||||||
"hardware": "1",
|
"hardware": "1",
|
||||||
"location": "08963fec7c53423ca5680aa4cb502c63",
|
"location": "12493538af164a409c6a1c79e38afe1c",
|
||||||
"model": "Tom/Floor",
|
"model": "Tom/Floor",
|
||||||
"name": "Thermostatic Radiator Badkamer",
|
"name": "Bios Cv Thermostatic Radiator ",
|
||||||
"zigbee_mac_address": "ABCD012345670A17",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"temperature": 19.1,
|
"battery": 62,
|
||||||
"setpoint": 14.0,
|
"setpoint": 13.0,
|
||||||
"battery": 51,
|
"temperature": 17.2,
|
||||||
"temperature_difference": -0.4,
|
"temperature_difference": -0.2,
|
||||||
"valve_position": 0.0,
|
"valve_position": 0.0,
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
},
|
},
|
||||||
"f1fee6043d3642a9b0a65297455f008e": {
|
|
||||||
"dev_class": "zone_thermostat",
|
|
||||||
"firmware": "2016-10-27T02:00:00+02:00",
|
|
||||||
"hardware": "255",
|
|
||||||
"location": "08963fec7c53423ca5680aa4cb502c63",
|
|
||||||
"model": "Lisa",
|
|
||||||
"name": "Zone Thermostat Badkamer",
|
|
||||||
"zigbee_mac_address": "ABCD012345670A08",
|
|
||||||
"vendor": "Plugwise",
|
"vendor": "Plugwise",
|
||||||
"thermostat": {
|
"zigbee_mac_address": "ABCD012345670A09",
|
||||||
"setpoint": 14.0,
|
|
||||||
"lower_bound": 0.0,
|
|
||||||
"upper_bound": 99.9,
|
|
||||||
"resolution": 0.01,
|
|
||||||
},
|
},
|
||||||
|
"b310b72a0e354bfab43089919b9a88bf": {
|
||||||
|
"available": True,
|
||||||
|
"dev_class": "thermo_sensor",
|
||||||
|
"firmware": "2019-03-27T01:00:00+01:00",
|
||||||
|
"hardware": "1",
|
||||||
|
"location": "c50f167537524366a5af7aa3942feb1e",
|
||||||
|
"model": "Tom/Floor",
|
||||||
|
"name": "Floor kraan",
|
||||||
|
"sensors": {
|
||||||
|
"setpoint": 21.5,
|
||||||
|
"temperature": 26.0,
|
||||||
|
"temperature_difference": 3.5,
|
||||||
|
"valve_position": 100,
|
||||||
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A02",
|
||||||
|
},
|
||||||
|
"b59bcebaf94b499ea7d46e4a66fb62d8": {
|
||||||
|
"active_preset": "home",
|
||||||
"available": True,
|
"available": True,
|
||||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
|
||||||
"active_preset": "away",
|
|
||||||
"available_schedules": [
|
"available_schedules": [
|
||||||
"CV Roan",
|
"CV Roan",
|
||||||
"Bios Schema met Film Avond",
|
"Bios Schema met Film Avond",
|
||||||
@ -345,46 +262,76 @@ async def test_diagnostics(
|
|||||||
"Badkamer Schema",
|
"Badkamer Schema",
|
||||||
"CV Jessie",
|
"CV Jessie",
|
||||||
],
|
],
|
||||||
"select_schedule": "Badkamer Schema",
|
"dev_class": "zone_thermostat",
|
||||||
"last_used": "Badkamer Schema",
|
"firmware": "2016-08-02T02:00:00+02:00",
|
||||||
|
"hardware": "255",
|
||||||
|
"last_used": "GF7 Woonkamer",
|
||||||
|
"location": "c50f167537524366a5af7aa3942feb1e",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92},
|
"model": "Lisa",
|
||||||
|
"name": "Zone Lisa WK",
|
||||||
|
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||||
|
"select_schedule": "GF7 Woonkamer",
|
||||||
|
"sensors": {"battery": 34, "setpoint": 21.5, "temperature": 20.9},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
},
|
},
|
||||||
"675416a629f343c495449970e2ca37b5": {
|
"thermostat": {
|
||||||
"dev_class": "router",
|
"lower_bound": 0.0,
|
||||||
|
"resolution": 0.01,
|
||||||
|
"setpoint": 21.5,
|
||||||
|
"upper_bound": 99.9,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A07",
|
||||||
|
},
|
||||||
|
"cd0ddb54ef694e11ac18ed1cbce5dbbd": {
|
||||||
|
"available": True,
|
||||||
|
"dev_class": "vcr",
|
||||||
"firmware": "2019-06-21T02:00:00+02:00",
|
"firmware": "2019-06-21T02:00:00+02:00",
|
||||||
"location": "cd143c07248f491493cea0533bc3d669",
|
"location": "cd143c07248f491493cea0533bc3d669",
|
||||||
"model": "Plug",
|
"model": "Plug",
|
||||||
"name": "Ziggo Modem",
|
"name": "NAS",
|
||||||
"zigbee_mac_address": "ABCD012345670A01",
|
|
||||||
"vendor": "Plugwise",
|
|
||||||
"available": True,
|
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"electricity_consumed": 12.2,
|
"electricity_consumed": 16.5,
|
||||||
"electricity_consumed_interval": 2.97,
|
"electricity_consumed_interval": 0.5,
|
||||||
"electricity_produced": 0.0,
|
"electricity_produced": 0.0,
|
||||||
"electricity_produced_interval": 0.0,
|
"electricity_produced_interval": 0.0,
|
||||||
},
|
},
|
||||||
"switches": {"relay": True, "lock": True},
|
"switches": {"lock": True, "relay": True},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A14",
|
||||||
},
|
},
|
||||||
"e7693eb9582644e5b865dba8d4447cf1": {
|
"d3da73bde12a47d5a6b8f9dad971f2ec": {
|
||||||
"dev_class": "thermostatic_radiator_valve",
|
"available": True,
|
||||||
|
"dev_class": "thermo_sensor",
|
||||||
"firmware": "2019-03-27T01:00:00+01:00",
|
"firmware": "2019-03-27T01:00:00+01:00",
|
||||||
"hardware": "1",
|
"hardware": "1",
|
||||||
"location": "446ac08dd04d4eff8ac57489757b7314",
|
"location": "82fa13f017d240daa0d0ea1775420f24",
|
||||||
"model": "Tom/Floor",
|
"model": "Tom/Floor",
|
||||||
"name": "CV Kraan Garage",
|
"name": "Thermostatic Radiator Jessie",
|
||||||
"zigbee_mac_address": "ABCD012345670A11",
|
"sensors": {
|
||||||
"vendor": "Plugwise",
|
"battery": 62,
|
||||||
"thermostat": {
|
"setpoint": 15.0,
|
||||||
"setpoint": 5.5,
|
"temperature": 17.1,
|
||||||
"lower_bound": 0.0,
|
"temperature_difference": 0.1,
|
||||||
"upper_bound": 100.0,
|
"valve_position": 0.0,
|
||||||
"resolution": 0.01,
|
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A10",
|
||||||
|
},
|
||||||
|
"df4a4a8169904cdb9c03d61a21f42140": {
|
||||||
|
"active_preset": "away",
|
||||||
"available": True,
|
"available": True,
|
||||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
|
||||||
"active_preset": "no_frost",
|
|
||||||
"available_schedules": [
|
"available_schedules": [
|
||||||
"CV Roan",
|
"CV Roan",
|
||||||
"Bios Schema met Film Avond",
|
"Bios Schema met Film Avond",
|
||||||
@ -392,16 +339,123 @@ async def test_diagnostics(
|
|||||||
"Badkamer Schema",
|
"Badkamer Schema",
|
||||||
"CV Jessie",
|
"CV Jessie",
|
||||||
],
|
],
|
||||||
"select_schedule": "None",
|
"dev_class": "zone_thermostat",
|
||||||
|
"firmware": "2016-10-27T02:00:00+02:00",
|
||||||
|
"hardware": "255",
|
||||||
"last_used": "Badkamer Schema",
|
"last_used": "Badkamer Schema",
|
||||||
|
"location": "12493538af164a409c6a1c79e38afe1c",
|
||||||
"mode": "heat",
|
"mode": "heat",
|
||||||
|
"model": "Lisa",
|
||||||
|
"name": "Zone Lisa Bios",
|
||||||
|
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||||
|
"select_schedule": "None",
|
||||||
|
"sensors": {"battery": 67, "setpoint": 13.0, "temperature": 16.5},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"thermostat": {
|
||||||
|
"lower_bound": 0.0,
|
||||||
|
"resolution": 0.01,
|
||||||
|
"setpoint": 13.0,
|
||||||
|
"upper_bound": 99.9,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A06",
|
||||||
|
},
|
||||||
|
"e7693eb9582644e5b865dba8d4447cf1": {
|
||||||
|
"active_preset": "no_frost",
|
||||||
|
"available": True,
|
||||||
|
"available_schedules": [
|
||||||
|
"CV Roan",
|
||||||
|
"Bios Schema met Film Avond",
|
||||||
|
"GF7 Woonkamer",
|
||||||
|
"Badkamer Schema",
|
||||||
|
"CV Jessie",
|
||||||
|
],
|
||||||
|
"dev_class": "thermostatic_radiator_valve",
|
||||||
|
"firmware": "2019-03-27T01:00:00+01:00",
|
||||||
|
"hardware": "1",
|
||||||
|
"last_used": "Badkamer Schema",
|
||||||
|
"location": "446ac08dd04d4eff8ac57489757b7314",
|
||||||
|
"mode": "heat",
|
||||||
|
"model": "Tom/Floor",
|
||||||
|
"name": "CV Kraan Garage",
|
||||||
|
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||||
|
"select_schedule": "None",
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"temperature": 15.6,
|
|
||||||
"setpoint": 5.5,
|
|
||||||
"battery": 68,
|
"battery": 68,
|
||||||
|
"setpoint": 5.5,
|
||||||
|
"temperature": 15.6,
|
||||||
"temperature_difference": 0.0,
|
"temperature_difference": 0.0,
|
||||||
"valve_position": 0.0,
|
"valve_position": 0.0,
|
||||||
},
|
},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"thermostat": {
|
||||||
|
"lower_bound": 0.0,
|
||||||
|
"resolution": 0.01,
|
||||||
|
"setpoint": 5.5,
|
||||||
|
"upper_bound": 100.0,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A11",
|
||||||
|
},
|
||||||
|
"f1fee6043d3642a9b0a65297455f008e": {
|
||||||
|
"active_preset": "away",
|
||||||
|
"available": True,
|
||||||
|
"available_schedules": [
|
||||||
|
"CV Roan",
|
||||||
|
"Bios Schema met Film Avond",
|
||||||
|
"GF7 Woonkamer",
|
||||||
|
"Badkamer Schema",
|
||||||
|
"CV Jessie",
|
||||||
|
],
|
||||||
|
"dev_class": "zone_thermostat",
|
||||||
|
"firmware": "2016-10-27T02:00:00+02:00",
|
||||||
|
"hardware": "255",
|
||||||
|
"last_used": "Badkamer Schema",
|
||||||
|
"location": "08963fec7c53423ca5680aa4cb502c63",
|
||||||
|
"mode": "auto",
|
||||||
|
"model": "Lisa",
|
||||||
|
"name": "Zone Thermostat Badkamer",
|
||||||
|
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||||
|
"select_schedule": "Badkamer Schema",
|
||||||
|
"sensors": {"battery": 92, "setpoint": 14.0, "temperature": 18.9},
|
||||||
|
"temperature_offset": {
|
||||||
|
"lower_bound": -2.0,
|
||||||
|
"resolution": 0.1,
|
||||||
|
"setpoint": 0.0,
|
||||||
|
"upper_bound": 2.0,
|
||||||
|
},
|
||||||
|
"thermostat": {
|
||||||
|
"lower_bound": 0.0,
|
||||||
|
"resolution": 0.01,
|
||||||
|
"setpoint": 14.0,
|
||||||
|
"upper_bound": 99.9,
|
||||||
|
},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670A08",
|
||||||
|
},
|
||||||
|
"fe799307f1624099878210aa0b9f1475": {
|
||||||
|
"binary_sensors": {"plugwise_notification": True},
|
||||||
|
"dev_class": "gateway",
|
||||||
|
"firmware": "3.0.15",
|
||||||
|
"hardware": "AME Smile 2.0 board",
|
||||||
|
"location": "1f9dcf83fd4e4b66b72ff787957bfe5d",
|
||||||
|
"mac_address": "012345670001",
|
||||||
|
"model": "Gateway",
|
||||||
|
"name": "Adam",
|
||||||
|
"select_regulation_mode": "heating",
|
||||||
|
"sensors": {"outdoor_temperature": 7.81},
|
||||||
|
"vendor": "Plugwise",
|
||||||
|
"zigbee_mac_address": "ABCD012345670101",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ async def test_anna_max_boiler_temp_change(
|
|||||||
|
|
||||||
assert mock_smile_anna.set_number_setpoint.call_count == 1
|
assert mock_smile_anna.set_number_setpoint.call_count == 1
|
||||||
mock_smile_anna.set_number_setpoint.assert_called_with(
|
mock_smile_anna.set_number_setpoint.assert_called_with(
|
||||||
"maximum_boiler_temperature", 65.0
|
"maximum_boiler_temperature", "1cbf783bb11e4a7c8a6843dee3a86927", 65.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -67,5 +67,5 @@ async def test_adam_dhw_setpoint_change(
|
|||||||
|
|
||||||
assert mock_smile_adam_2.set_number_setpoint.call_count == 1
|
assert mock_smile_adam_2.set_number_setpoint.call_count == 1
|
||||||
mock_smile_adam_2.set_number_setpoint.assert_called_with(
|
mock_smile_adam_2.set_number_setpoint.assert_called_with(
|
||||||
"max_dhw_temperature", 55.0
|
"max_dhw_temperature", "056ee145a816487eaa69243c3280f8bf", 55.0
|
||||||
)
|
)
|
||||||
|
@ -1835,3 +1835,35 @@ async def test_entity_id_update_discovery_update(
|
|||||||
await help_test_entity_id_update_discovery_update(
|
await help_test_entity_id_update_discovery_update(
|
||||||
hass, mqtt_mock, Platform.LIGHT, config
|
hass, mqtt_mock, Platform.LIGHT, config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_device_name(
|
||||||
|
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||||
|
) -> None:
|
||||||
|
"""Test name of lights when no device name is set.
|
||||||
|
|
||||||
|
When the device name is not set, Tasmota uses friendly name 1 as device naem.
|
||||||
|
This test ensures that case is handled correctly.
|
||||||
|
"""
|
||||||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
|
config["dn"] = "Light 1"
|
||||||
|
config["fn"][0] = "Light 1"
|
||||||
|
config["fn"][1] = "Light 2"
|
||||||
|
config["rl"][0] = 2
|
||||||
|
config["rl"][1] = 2
|
||||||
|
mac = config["mac"]
|
||||||
|
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||||||
|
json.dumps(config),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("light.light_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["friendly_name"] == "Light 1"
|
||||||
|
|
||||||
|
state = hass.states.get("light.light_1_light_2")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["friendly_name"] == "Light 1 Light 2"
|
||||||
|
@ -137,6 +137,27 @@ DICT_SENSOR_CONFIG_2 = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NUMBERED_SENSOR_CONFIG = {
|
||||||
|
"sn": {
|
||||||
|
"Time": "2020-09-25T12:47:15",
|
||||||
|
"ANALOG": {
|
||||||
|
"Temperature1": 2.4,
|
||||||
|
"Temperature2": 2.4,
|
||||||
|
"Illuminance3": 2.4,
|
||||||
|
},
|
||||||
|
"TempUnit": "C",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NUMBERED_SENSOR_CONFIG_2 = {
|
||||||
|
"sn": {
|
||||||
|
"Time": "2020-09-25T12:47:15",
|
||||||
|
"ANALOG": {
|
||||||
|
"CTEnergy1": {"Energy": 0.5, "Power": 2300, "Voltage": 230, "Current": 10},
|
||||||
|
},
|
||||||
|
"TempUnit": "C",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEMPERATURE_SENSOR_CONFIG = {
|
TEMPERATURE_SENSOR_CONFIG = {
|
||||||
"sn": {
|
"sn": {
|
||||||
@ -343,6 +364,118 @@ TEMPERATURE_SENSOR_CONFIG = {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
NUMBERED_SENSOR_CONFIG,
|
||||||
|
[
|
||||||
|
"sensor.tasmota_analog_temperature1",
|
||||||
|
"sensor.tasmota_analog_temperature2",
|
||||||
|
"sensor.tasmota_analog_illuminance3",
|
||||||
|
],
|
||||||
|
(
|
||||||
|
(
|
||||||
|
'{"ANALOG":{"Temperature1":1.2,"Temperature2":3.4,'
|
||||||
|
'"Illuminance3": 5.6}}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'{"StatusSNS":{"ANALOG":{"Temperature1": 7.8,"Temperature2": 9.0,'
|
||||||
|
'"Illuminance3":1.2}}}'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"sensor.tasmota_analog_temperature1": {
|
||||||
|
"state": "1.2",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "temperature",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "°C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.tasmota_analog_temperature2": {
|
||||||
|
"state": "3.4",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "temperature",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "°C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.tasmota_analog_illuminance3": {
|
||||||
|
"state": "5.6",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "illuminance",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "lx",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sensor.tasmota_analog_temperature1": {"state": "7.8"},
|
||||||
|
"sensor.tasmota_analog_temperature2": {"state": "9.0"},
|
||||||
|
"sensor.tasmota_analog_illuminance3": {"state": "1.2"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NUMBERED_SENSOR_CONFIG_2,
|
||||||
|
[
|
||||||
|
"sensor.tasmota_analog_ctenergy1_energy",
|
||||||
|
"sensor.tasmota_analog_ctenergy1_power",
|
||||||
|
"sensor.tasmota_analog_ctenergy1_voltage",
|
||||||
|
"sensor.tasmota_analog_ctenergy1_current",
|
||||||
|
],
|
||||||
|
(
|
||||||
|
(
|
||||||
|
'{"ANALOG":{"CTEnergy1":'
|
||||||
|
'{"Energy":0.5,"Power":2300,"Voltage":230,"Current":10}}}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'{"StatusSNS":{"ANALOG":{"CTEnergy1":'
|
||||||
|
'{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"sensor.tasmota_analog_ctenergy1_energy": {
|
||||||
|
"state": "0.5",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "energy",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_power": {
|
||||||
|
"state": "2300",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "power",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_voltage": {
|
||||||
|
"state": "230",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "voltage",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "V",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_current": {
|
||||||
|
"state": "10",
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "current",
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
|
"unit_of_measurement": "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_power": {"state": "1150"},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"},
|
||||||
|
"sensor.tasmota_analog_ctenergy1_current": {"state": "5"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_controlling_state_via_mqtt(
|
async def test_controlling_state_via_mqtt(
|
||||||
@ -409,6 +542,87 @@ async def test_controlling_state_via_mqtt(
|
|||||||
assert state.attributes.get(attribute) == expected
|
assert state.attributes.get(attribute) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("sensor_config", "entity_ids", "states"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
# The AS33935 energy sensor is not reporting energy in W
|
||||||
|
{"sn": {"Time": "2020-09-25T12:47:15", "AS3935": {"Energy": None}}},
|
||||||
|
["sensor.tasmota_as3935_energy"],
|
||||||
|
{
|
||||||
|
"sensor.tasmota_as3935_energy": {
|
||||||
|
"device_class": None,
|
||||||
|
"state_class": None,
|
||||||
|
"unit_of_measurement": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
# The AS33935 energy sensor is not reporting energy in W
|
||||||
|
{"sn": {"Time": "2020-09-25T12:47:15", "LD2410": {"Energy": None}}},
|
||||||
|
["sensor.tasmota_ld2410_energy"],
|
||||||
|
{
|
||||||
|
"sensor.tasmota_ld2410_energy": {
|
||||||
|
"device_class": None,
|
||||||
|
"state_class": None,
|
||||||
|
"unit_of_measurement": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
# Check other energy sensors work
|
||||||
|
{"sn": {"Time": "2020-09-25T12:47:15", "Other": {"Energy": None}}},
|
||||||
|
["sensor.tasmota_other_energy"],
|
||||||
|
{
|
||||||
|
"sensor.tasmota_other_energy": {
|
||||||
|
"device_class": "energy",
|
||||||
|
"state_class": "total",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_quantity_override(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock: MqttMockHAClient,
|
||||||
|
setup_tasmota,
|
||||||
|
sensor_config,
|
||||||
|
entity_ids,
|
||||||
|
states,
|
||||||
|
) -> None:
|
||||||
|
"""Test quantity override for certain sensors."""
|
||||||
|
entity_reg = er.async_get(hass)
|
||||||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
|
sensor_config = copy.deepcopy(sensor_config)
|
||||||
|
mac = config["mac"]
|
||||||
|
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||||||
|
json.dumps(config),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
f"{DEFAULT_PREFIX}/{mac}/sensors",
|
||||||
|
json.dumps(sensor_config),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for entity_id in entity_ids:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "unavailable"
|
||||||
|
expected_state = states[entity_id]
|
||||||
|
for attribute, expected in expected_state.get("attributes", {}).items():
|
||||||
|
assert state.attributes.get(attribute) == expected
|
||||||
|
|
||||||
|
entry = entity_reg.async_get(entity_id)
|
||||||
|
assert entry.disabled is False
|
||||||
|
assert entry.disabled_by is None
|
||||||
|
assert entry.entity_category is None
|
||||||
|
|
||||||
|
|
||||||
async def test_bad_indexed_sensor_state_via_mqtt(
|
async def test_bad_indexed_sensor_state_via_mqtt(
|
||||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -283,3 +283,35 @@ async def test_entity_id_update_discovery_update(
|
|||||||
await help_test_entity_id_update_discovery_update(
|
await help_test_entity_id_update_discovery_update(
|
||||||
hass, mqtt_mock, Platform.SWITCH, config
|
hass, mqtt_mock, Platform.SWITCH, config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_device_name(
|
||||||
|
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||||
|
) -> None:
|
||||||
|
"""Test name of switches when no device name is set.
|
||||||
|
|
||||||
|
When the device name is not set, Tasmota uses friendly name 1 as device naem.
|
||||||
|
This test ensures that case is handled correctly.
|
||||||
|
"""
|
||||||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
|
config["dn"] = "Relay 1"
|
||||||
|
config["fn"][0] = "Relay 1"
|
||||||
|
config["fn"][1] = "Relay 2"
|
||||||
|
config["rl"][0] = 1
|
||||||
|
config["rl"][1] = 1
|
||||||
|
mac = config["mac"]
|
||||||
|
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass,
|
||||||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||||||
|
json.dumps(config),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("switch.relay_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["friendly_name"] == "Relay 1"
|
||||||
|
|
||||||
|
state = hass.states.get("switch.relay_1_relay_2")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["friendly_name"] == "Relay 1 Relay 2"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""The test for the Template sensor platform."""
|
"""The test for the Template sensor platform."""
|
||||||
from asyncio import Event
|
from asyncio import Event
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import ANY, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -1140,6 +1140,48 @@ async def test_trigger_entity(
|
|||||||
assert state.context is context
|
assert state.context is context
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"template": [
|
||||||
|
{
|
||||||
|
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||||
|
"sensors": {
|
||||||
|
"hello": {
|
||||||
|
"friendly_name": "Hello Name",
|
||||||
|
"value_template": "{{ trigger.event.data.beer }}",
|
||||||
|
"entity_picture_template": "{{ '/local/dogs.png' }}",
|
||||||
|
"icon_template": "{{ 'mdi:pirate' }}",
|
||||||
|
"attribute_templates": {
|
||||||
|
"last": "{{now().strftime('%D %X')}}",
|
||||||
|
"history_1": "{{this.attributes.last|default('Not yet set')}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_trigger_entity_runs_once(
|
||||||
|
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test trigger entity handles a trigger once."""
|
||||||
|
state = hass.states.get("sensor.hello_name")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
hass.bus.async_fire("test_event", {"beer": 2})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.hello_name")
|
||||||
|
assert state.state == "2"
|
||||||
|
assert state.attributes.get("last") == ANY
|
||||||
|
assert state.attributes.get("history_1") == "Not yet set"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"config",
|
"config",
|
||||||
|
@ -293,14 +293,20 @@ def zigpy_device_mock(zigpy_app_controller):
|
|||||||
return _mock_dev
|
return _mock_dev
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True))
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def zha_device_joined(hass, setup_zha):
|
def zha_device_joined(hass, setup_zha):
|
||||||
"""Return a newly joined ZHA device."""
|
"""Return a newly joined ZHA device."""
|
||||||
|
setup_zha_fixture = setup_zha
|
||||||
|
|
||||||
async def _zha_device(zigpy_dev):
|
async def _zha_device(zigpy_dev, *, setup_zha: bool = True):
|
||||||
zigpy_dev.last_seen = time.time()
|
zigpy_dev.last_seen = time.time()
|
||||||
await setup_zha()
|
|
||||||
|
if setup_zha:
|
||||||
|
await setup_zha_fixture()
|
||||||
|
|
||||||
zha_gateway = common.get_zha_gateway(hass)
|
zha_gateway = common.get_zha_gateway(hass)
|
||||||
|
zha_gateway.application_controller.devices[zigpy_dev.ieee] = zigpy_dev
|
||||||
await zha_gateway.async_device_initialized(zigpy_dev)
|
await zha_gateway.async_device_initialized(zigpy_dev)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
return zha_gateway.get_device(zigpy_dev.ieee)
|
return zha_gateway.get_device(zigpy_dev.ieee)
|
||||||
@ -308,17 +314,21 @@ def zha_device_joined(hass, setup_zha):
|
|||||||
return _zha_device
|
return _zha_device
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True))
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def zha_device_restored(hass, zigpy_app_controller, setup_zha):
|
def zha_device_restored(hass, zigpy_app_controller, setup_zha):
|
||||||
"""Return a restored ZHA device."""
|
"""Return a restored ZHA device."""
|
||||||
|
setup_zha_fixture = setup_zha
|
||||||
|
|
||||||
async def _zha_device(zigpy_dev, last_seen=None):
|
async def _zha_device(zigpy_dev, *, last_seen=None, setup_zha: bool = True):
|
||||||
zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev
|
zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev
|
||||||
|
|
||||||
if last_seen is not None:
|
if last_seen is not None:
|
||||||
zigpy_dev.last_seen = last_seen
|
zigpy_dev.last_seen = last_seen
|
||||||
|
|
||||||
await setup_zha()
|
if setup_zha:
|
||||||
|
await setup_zha_fixture()
|
||||||
|
|
||||||
zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
|
zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
|
||||||
return zha_gateway.get_device(zigpy_dev.ieee)
|
return zha_gateway.get_device(zigpy_dev.ieee)
|
||||||
|
|
||||||
@ -376,3 +386,10 @@ def hass_disable_services(hass):
|
|||||||
hass, "services", MagicMock(has_service=MagicMock(return_value=True))
|
hass, "services", MagicMock(has_service=MagicMock(return_value=True))
|
||||||
):
|
):
|
||||||
yield hass
|
yield hass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def speed_up_radio_mgr():
|
||||||
|
"""Speed up the radio manager connection time by removing delays."""
|
||||||
|
with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.00001):
|
||||||
|
yield
|
||||||
|
@ -62,13 +62,6 @@ def mock_multipan_platform():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def reduce_reconnect_timeout():
|
|
||||||
"""Reduces reconnect timeout to speed up tests."""
|
|
||||||
with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.01):
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_app():
|
def mock_app():
|
||||||
"""Mock zigpy app interface."""
|
"""Mock zigpy app interface."""
|
||||||
|
@ -9,6 +9,9 @@ import zigpy.zcl.clusters.general as general
|
|||||||
|
|
||||||
import homeassistant.components.automation as automation
|
import homeassistant.components.automation as automation
|
||||||
from homeassistant.components.device_automation import DeviceAutomationType
|
from homeassistant.components.device_automation import DeviceAutomationType
|
||||||
|
from homeassistant.components.device_automation.exceptions import (
|
||||||
|
InvalidDeviceAutomationConfig,
|
||||||
|
)
|
||||||
from homeassistant.components.zha.core.const import ATTR_ENDPOINT_ID
|
from homeassistant.components.zha.core.const import ATTR_ENDPOINT_ID
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -20,6 +23,7 @@ from .common import async_enable_traffic
|
|||||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
async_fire_time_changed,
|
async_fire_time_changed,
|
||||||
async_get_device_automations,
|
async_get_device_automations,
|
||||||
async_mock_service,
|
async_mock_service,
|
||||||
@ -45,6 +49,16 @@ LONG_PRESS = "remote_button_long_press"
|
|||||||
LONG_RELEASE = "remote_button_long_release"
|
LONG_RELEASE = "remote_button_long_release"
|
||||||
|
|
||||||
|
|
||||||
|
SWITCH_SIGNATURE = {
|
||||||
|
1: {
|
||||||
|
SIG_EP_INPUT: [general.Basic.cluster_id],
|
||||||
|
SIG_EP_OUTPUT: [general.OnOff.cluster_id],
|
||||||
|
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
|
||||||
|
SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def sensor_platforms_only():
|
def sensor_platforms_only():
|
||||||
"""Only set up the sensor platform and required base platforms to speed up tests."""
|
"""Only set up the sensor platform and required base platforms to speed up tests."""
|
||||||
@ -72,16 +86,7 @@ def calls(hass):
|
|||||||
async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored):
|
async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored):
|
||||||
"""IAS device fixture."""
|
"""IAS device fixture."""
|
||||||
|
|
||||||
zigpy_device = zigpy_device_mock(
|
zigpy_device = zigpy_device_mock(SWITCH_SIGNATURE)
|
||||||
{
|
|
||||||
1: {
|
|
||||||
SIG_EP_INPUT: [general.Basic.cluster_id],
|
|
||||||
SIG_EP_OUTPUT: [general.OnOff.cluster_id],
|
|
||||||
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
|
|
||||||
SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
zha_device = await zha_device_joined_restored(zigpy_device)
|
zha_device = await zha_device_joined_restored(zigpy_device)
|
||||||
zha_device.update_available(True)
|
zha_device.update_available(True)
|
||||||
@ -397,3 +402,108 @@ async def test_exception_bad_trigger(
|
|||||||
"Unnamed automation failed to setup triggers and has been disabled: "
|
"Unnamed automation failed to setup triggers and has been disabled: "
|
||||||
"device does not have trigger ('junk', 'junk')" in caplog.text
|
"device does not have trigger ('junk', 'junk')" in caplog.text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validate_trigger_config_missing_info(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
zigpy_device_mock,
|
||||||
|
mock_zigpy_connect,
|
||||||
|
zha_device_joined,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device triggers referring to a missing device."""
|
||||||
|
|
||||||
|
# Join a device
|
||||||
|
switch = zigpy_device_mock(SWITCH_SIGNATURE)
|
||||||
|
await zha_device_joined(switch)
|
||||||
|
|
||||||
|
# After we unload the config entry, trigger info was not cached on startup, nor can
|
||||||
|
# it be pulled from the current device, making it impossible to validate triggers
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
ha_device_registry = dr.async_get(hass)
|
||||||
|
reg_device = ha_device_registry.async_get_device(
|
||||||
|
identifiers={("zha", str(switch.ieee))}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"device_id": reg_device.id,
|
||||||
|
"domain": "zha",
|
||||||
|
"platform": "device",
|
||||||
|
"type": "junk",
|
||||||
|
"subtype": "junk",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data": {"message": "service called"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Unable to get zha device" in caplog.text
|
||||||
|
|
||||||
|
with pytest.raises(InvalidDeviceAutomationConfig):
|
||||||
|
await async_get_device_automations(
|
||||||
|
hass, DeviceAutomationType.TRIGGER, reg_device.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validate_trigger_config_unloaded_bad_info(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
zigpy_device_mock,
|
||||||
|
mock_zigpy_connect,
|
||||||
|
zha_device_joined,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test device triggers referring to a missing device."""
|
||||||
|
|
||||||
|
# Join a device
|
||||||
|
switch = zigpy_device_mock(SWITCH_SIGNATURE)
|
||||||
|
await zha_device_joined(switch)
|
||||||
|
|
||||||
|
# After we unload the config entry, trigger info was not cached on startup, nor can
|
||||||
|
# it be pulled from the current device, making it impossible to validate triggers
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
# Reload ZHA to persist the device info in the cache
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
ha_device_registry = dr.async_get(hass)
|
||||||
|
reg_device = ha_device_registry.async_get_device(
|
||||||
|
identifiers={("zha", str(switch.ieee))}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"device_id": reg_device.id,
|
||||||
|
"domain": "zha",
|
||||||
|
"platform": "device",
|
||||||
|
"type": "junk",
|
||||||
|
"subtype": "junk",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data": {"message": "service called"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Unable to find trigger" in caplog.text
|
||||||
|
@ -6,7 +6,6 @@ import pytest
|
|||||||
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
|
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
|
||||||
from zigpy.exceptions import TransientConnectionError
|
from zigpy.exceptions import TransientConnectionError
|
||||||
|
|
||||||
from homeassistant.components.zha import async_setup_entry
|
|
||||||
from homeassistant.components.zha.core.const import (
|
from homeassistant.components.zha.core.const import (
|
||||||
CONF_BAUDRATE,
|
CONF_BAUDRATE,
|
||||||
CONF_RADIO_TYPE,
|
CONF_RADIO_TYPE,
|
||||||
@ -22,7 +21,7 @@ from .test_light import LIGHT_ON_OFF
|
|||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
DATA_RADIO_TYPE = "deconz"
|
DATA_RADIO_TYPE = "ezsp"
|
||||||
DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0"
|
DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0"
|
||||||
|
|
||||||
|
|
||||||
@ -137,7 +136,7 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None:
|
|||||||
"homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True)
|
"homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True)
|
||||||
)
|
)
|
||||||
async def test_setup_with_v3_cleaning_uri(
|
async def test_setup_with_v3_cleaning_uri(
|
||||||
hass: HomeAssistant, path: str, cleaned_path: str
|
hass: HomeAssistant, path: str, cleaned_path: str, mock_zigpy_connect
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test migration of config entry from v3, applying corrections to the port path."""
|
"""Test migration of config entry from v3, applying corrections to the port path."""
|
||||||
config_entry_v3 = MockConfigEntry(
|
config_entry_v3 = MockConfigEntry(
|
||||||
@ -150,14 +149,9 @@ async def test_setup_with_v3_cleaning_uri(
|
|||||||
)
|
)
|
||||||
config_entry_v3.add_to_hass(hass)
|
config_entry_v3.add_to_hass(hass)
|
||||||
|
|
||||||
with patch(
|
await hass.config_entries.async_setup(config_entry_v3.entry_id)
|
||||||
"homeassistant.components.zha.ZHAGateway", return_value=AsyncMock()
|
await hass.async_block_till_done()
|
||||||
) as mock_gateway:
|
await hass.config_entries.async_unload(config_entry_v3.entry_id)
|
||||||
mock_gateway.return_value.coordinator_ieee = "mock_ieee"
|
|
||||||
mock_gateway.return_value.radio_description = "mock_radio"
|
|
||||||
|
|
||||||
assert await async_setup_entry(hass, config_entry_v3)
|
|
||||||
hass.data[DOMAIN]["zha_gateway"] = mock_gateway.return_value
|
|
||||||
|
|
||||||
assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
|
assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
|
||||||
assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path
|
assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path
|
||||||
|
@ -32,9 +32,7 @@ def disable_platform_only():
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reduce_reconnect_timeout():
|
def reduce_reconnect_timeout():
|
||||||
"""Reduces reconnect timeout to speed up tests."""
|
"""Reduces reconnect timeout to speed up tests."""
|
||||||
with patch(
|
with patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001):
|
||||||
"homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001
|
|
||||||
), patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001):
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +97,7 @@ def mock_connect_zigpy_app() -> Generator[MagicMock, None, None]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
|
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
|
||||||
return_value=mock_connect_app,
|
return_value=mock_connect_app,
|
||||||
):
|
):
|
||||||
yield mock_connect_app
|
yield mock_connect_app
|
||||||
|
@ -22,16 +22,10 @@ import homeassistant.helpers.issue_registry as ir
|
|||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
async def test_device_config_file_changed(
|
async def _trigger_repair_issue(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant, client, multisensor_6_state
|
||||||
hass_client: ClientSessionGenerator,
|
) -> Node:
|
||||||
hass_ws_client: WebSocketGenerator,
|
"""Trigger repair issue."""
|
||||||
client,
|
|
||||||
multisensor_6_state,
|
|
||||||
integration,
|
|
||||||
) -> None:
|
|
||||||
"""Test the device_config_file_changed issue."""
|
|
||||||
dev_reg = dr.async_get(hass)
|
|
||||||
# Create a node
|
# Create a node
|
||||||
node_state = deepcopy(multisensor_6_state)
|
node_state = deepcopy(multisensor_6_state)
|
||||||
node = Node(client, node_state)
|
node = Node(client, node_state)
|
||||||
@ -53,6 +47,23 @@ async def test_device_config_file_changed(
|
|||||||
|
|
||||||
client.async_send_command_no_wait.reset_mock()
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_config_file_changed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
client,
|
||||||
|
multisensor_6_state,
|
||||||
|
integration,
|
||||||
|
) -> None:
|
||||||
|
"""Test the device_config_file_changed issue."""
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
node = await _trigger_repair_issue(hass, client, multisensor_6_state)
|
||||||
|
|
||||||
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
|
||||||
device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)})
|
device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)})
|
||||||
assert device
|
assert device
|
||||||
issue_id = f"device_config_file_changed.{device.id}"
|
issue_id = f"device_config_file_changed.{device.id}"
|
||||||
@ -157,3 +168,46 @@ async def test_invalid_issue(
|
|||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert len(msg["result"]["issues"]) == 0
|
assert len(msg["result"]["issues"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_confirm(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
client,
|
||||||
|
multisensor_6_state,
|
||||||
|
integration,
|
||||||
|
) -> None:
|
||||||
|
"""Test aborting device_config_file_changed issue in confirm step."""
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
node = await _trigger_repair_issue(hass, client, multisensor_6_state)
|
||||||
|
|
||||||
|
device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)})
|
||||||
|
assert device
|
||||||
|
issue_id = f"device_config_file_changed.{device.id}"
|
||||||
|
|
||||||
|
await async_process_repairs_platforms(hass)
|
||||||
|
await hass_ws_client(hass)
|
||||||
|
http_client = await hass_client()
|
||||||
|
|
||||||
|
url = RepairsFlowIndexView.url
|
||||||
|
resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
assert data["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# Unload config entry so we can't connect to the node
|
||||||
|
await hass.config_entries.async_unload(integration.entry_id)
|
||||||
|
|
||||||
|
# Apply fix
|
||||||
|
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
|
||||||
|
resp = await http_client.post(url)
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
assert data["type"] == "abort"
|
||||||
|
assert data["reason"] == "cannot_connect"
|
||||||
|
assert data["description_placeholders"] == {"device_name": device.name}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user