This commit is contained in:
Paulus Schoutsen 2023-09-12 15:41:50 -04:00 committed by GitHub
commit c6ed235010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2105 additions and 618 deletions

View File

@ -5,25 +5,35 @@ import logging
from airthings_ble import AirthingsDevice
from homeassistant import config_entries
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.core import HomeAssistant, callback
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_registry import (
RegistryEntry,
async_entries_for_device,
async_get as entity_async_get,
)
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
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(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airthings BLE sensors."""
@ -137,6 +181,7 @@ async def async_setup_entry(
sensor_value,
)
continue
async_migrate(hass, coordinator.data.address, sensor_type)
entities.append(
AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
)
@ -165,7 +210,7 @@ class AirthingsSensor(
if identifier := airthings_device.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(
connections={
(

View File

@ -14,10 +14,10 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==0.21.0",
"bleak==0.21.1",
"bleak-retry-connector==3.1.3",
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.2",
"bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.11.0",
"dbus-fast==1.95.2"
]

View File

@ -47,7 +47,6 @@ from .const import (
CONF_FILTER,
CONF_GOOGLE_ACTIONS,
CONF_RELAYER_SERVER,
CONF_REMOTE_SNI_SERVER,
CONF_REMOTESTATE_SERVER,
CONF_SERVICEHANDLERS_SERVER,
CONF_THINGTALK_SERVER,
@ -115,7 +114,6 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_ALEXA_SERVER): str,
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,
vol.Optional(CONF_REMOTE_SNI_SERVER): str,
vol.Optional(CONF_REMOTESTATE_SERVER): str,
vol.Optional(CONF_THINGTALK_SERVER): str,
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,

View File

@ -55,7 +55,6 @@ CONF_ACME_SERVER = "acme_server"
CONF_ALEXA_SERVER = "alexa_server"
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
CONF_RELAYER_SERVER = "relayer_server"
CONF_REMOTE_SNI_SERVER = "remote_sni_server"
CONF_REMOTESTATE_SERVER = "remotestate_server"
CONF_THINGTALK_SERVER = "thingtalk_server"
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"

View File

@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.70.0"]
"requirements": ["hass-nabucasa==0.71.0"]
}

View File

@ -44,7 +44,6 @@ class ComelitSerialBridge(DataUpdateCoordinator):
raise ConfigEntryAuthFailed
devices_data = await self.api.get_all_devices()
alarm_data = await self.api.get_alarm_config()
await self.api.logout()
return devices_data | alarm_data
return devices_data

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
_LOGGER = logging.getLogger(__name__)
HOP_SCAN_INTERVAL = timedelta(hours=2)
HOP_SCAN_INTERVAL = timedelta(minutes=20)
class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):

View File

@ -3,7 +3,7 @@
"flow_title": "{serial} ({host})",
"step": {
"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": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",

View File

@ -1096,7 +1096,7 @@ class FritzBoxBaseEntity:
class FritzRequireKeysMixin:
"""Fritz entity description mix in."""
value_fn: Callable[[FritzStatus, Any], Any]
value_fn: Callable[[FritzStatus, Any], Any] | None
@dataclass
@ -1118,8 +1118,11 @@ class FritzBoxBaseCoordinatorEntity(update_coordinator.CoordinatorEntity[AvmWrap
) -> None:
"""Init device info class."""
super().__init__(avm_wrapper)
if description.value_fn is not None:
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._device_name = device_name

View File

@ -1,20 +1,31 @@
"""Support for AVM FRITZ!Box update platform."""
from __future__ import annotations
from dataclasses import dataclass
import logging
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.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import AvmWrapper, FritzBoxBaseEntity
from .common import AvmWrapper, FritzBoxBaseCoordinatorEntity, FritzEntityDescription
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):
"""Describes Fritz update entity."""
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@ -27,11 +38,13 @@ async def async_setup_entry(
async_add_entities(entities)
class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity):
class FritzBoxUpdateEntity(FritzBoxBaseCoordinatorEntity, UpdateEntity):
"""Mixin for update entity specific attributes."""
_attr_entity_category = EntityCategory.CONFIG
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_title = "FRITZ!OS"
entity_description: FritzUpdateEntityDescription
def __init__(
self,
@ -39,29 +52,30 @@ class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity):
device_friendly_name: str,
) -> None:
"""Init FRITZ!Box connectivity class."""
self._attr_name = f"{device_friendly_name} FRITZ!OS"
self._attr_unique_id = f"{avm_wrapper.unique_id}-update"
super().__init__(avm_wrapper, device_friendly_name)
description = FritzUpdateEntityDescription(
key="update", name="FRITZ!OS", value_fn=None
)
super().__init__(avm_wrapper, device_friendly_name, description)
@property
def installed_version(self) -> str | None:
"""Version currently in use."""
return self._avm_wrapper.current_firmware
return self.coordinator.current_firmware
@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
if self._avm_wrapper.update_available:
return self._avm_wrapper.latest_firmware
return self._avm_wrapper.current_firmware
if self.coordinator.update_available:
return self.coordinator.latest_firmware
return self.coordinator.current_firmware
@property
def release_url(self) -> str | None:
"""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(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
await self._avm_wrapper.async_trigger_firmware_update()
await self.coordinator.async_trigger_firmware_update()

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230908.0"]
"requirements": ["home-assistant-frontend==20230911.0"]
}

View File

@ -6,6 +6,7 @@ from contextlib import suppress
from datetime import datetime, timedelta
import logging
import os
import re
from typing import Any, NamedTuple
import voluptuous as vol
@ -149,10 +150,12 @@ SERVICE_BACKUP_PARTIAL = "backup_partial"
SERVICE_RESTORE_FULL = "restore_full"
SERVICE_RESTORE_PARTIAL = "restore_partial"
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug."""
value = cv.slug(value)
value = VALID_ADDON_SLUG(value)
hass: HomeAssistant | None = None
with suppress(HomeAssistantError):

View File

@ -116,8 +116,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
name = discovery_info.name.replace(f".{zctype}", "")
tls = zctype == "_ipps._tcp.local."
base_path = discovery_info.properties.get("rp", "ipp/print")
self.context.update({"title_placeholders": {"name": name}})
unique_id = discovery_info.properties.get("UUID")
self.discovery_info.update(
{
@ -127,10 +126,18 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_VERIFY_SSL: False,
CONF_BASE_PATH: f"/{base_path}",
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:
info = await validate_input(self.hass, self.discovery_info)
except IPPConnectionUpgradeRequired:
@ -147,7 +154,6 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("IPP Error", exc_info=True)
return self.async_abort(reason="ipp_error")
unique_id = self.discovery_info[CONF_UUID]
if not unique_id and info[CONF_UUID]:
_LOGGER.debug(
"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"
)
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)
self._abort_if_unique_id_configured(
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(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:

View File

@ -6,5 +6,5 @@
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter",
"iot_class": "local_polling",
"requirements": ["ultraheat-api==0.5.1"]
"requirements": ["ultraheat-api==0.5.7"]
}

View File

@ -31,7 +31,6 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
ACTIVE_SCAN_INTERVAL,
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
@ -116,8 +115,9 @@ class BasePlatform(Entity):
def async_run(self) -> None:
"""Remote start entity."""
self.async_hold(update=False)
if self._scan_interval == 0 or self._scan_interval > ACTIVE_SCAN_INTERVAL:
self._cancel_call = async_call_later(self.hass, 1, self.async_update)
self._cancel_call = async_call_later(
self.hass, timedelta(milliseconds=100), self.async_update
)
if self._scan_interval > 0:
self._cancel_timer = async_track_time_interval(
self.hass, self.async_update, timedelta(seconds=self._scan_interval)
@ -188,10 +188,14 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
registers.reverse()
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."""
if self._nan_value and entry in (self._nan_value, -self._nan_value):
return None
if isinstance(entry, bytes):
return entry
val: float | int = self._scale * entry + self._offset
if self._min_value is not None and val < self._min_value:
return self._min_value
@ -232,14 +236,20 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
if isinstance(v_temp, int) and self._precision == 0:
v_result.append(str(v_temp))
elif v_temp is None:
v_result.append("") # pragma: no cover
v_result.append("0")
elif v_temp != v_temp: # noqa: PLR0124
# NaN float detection replace with None
v_result.append("nan") # pragma: no cover
v_result.append("0")
else:
v_result.append(f"{float(v_temp):.{self._precision}f}")
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
val_result = self.__process_raw_value(val[0])
@ -249,15 +259,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
if val_result is 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:
return str(val_result)
if isinstance(val_result, str):
if val_result == "nan":
val_result = None # pragma: no cover
return val_result
if isinstance(val_result, bytes):
return val_result.decode()
return f"{float(val_result):.{self._precision}f}"

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "gold",
"requirements": ["pymodbus==3.5.1"]
"requirements": ["pymodbus==3.5.2"]
}

View File

@ -1,7 +1,7 @@
"""Support for Modbus Register sensors."""
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timedelta
import logging
from typing import Any
@ -19,6 +19,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
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.update_coordinator import (
CoordinatorEntity,
@ -106,12 +107,16 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
"""Update the state of the sensor."""
# remark "now" is a dummy parameter to avoid problems with
# async_track_time_interval
self._cancel_call = None
raw_result = await self._hub.async_pb_call(
self._slave, self._address, self._count, self._input_type
)
if raw_result is None:
if self._lazy_errors:
self._lazy_errors -= 1
self._cancel_call = async_call_later(
self.hass, timedelta(seconds=1), self.async_update
)
return
self._lazy_errors = self._lazy_error_count
self._attr_available = False

View File

@ -1135,6 +1135,11 @@ class MqttEntity(
elif not self._default_to_device_class_name():
# Assign the 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:
device_name: str
if CONF_NAME not in config[CONF_DEVICE]:

View File

@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["crcmod", "plugwise"],
"requirements": ["plugwise==0.31.9"],
"requirements": ["plugwise==0.32.2"],
"zeroconf": ["_plugwise._tcp.local."]
}

View File

@ -27,7 +27,7 @@ from .entity import PlugwiseEntity
class PlugwiseEntityDescriptionMixin:
"""Mixin values for Plugwise entities."""
command: Callable[[Smile, str, float], Awaitable[None]]
command: Callable[[Smile, str, str, float], Awaitable[None]]
@dataclass
@ -43,7 +43,9 @@ NUMBER_TYPES = (
PlugwiseNumberEntityDescription(
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,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -51,7 +53,9 @@ NUMBER_TYPES = (
PlugwiseNumberEntityDescription(
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,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -94,6 +98,7 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
) -> None:
"""Initiate Plugwise Number."""
super().__init__(coordinator, device_id)
self.device_id = device_id
self.entity_description = description
self._attr_unique_id = f"{device_id}-{description.key}"
self._attr_mode = NumberMode.BOX
@ -109,6 +114,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None:
"""Change to the new setpoint value."""
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()

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/python_script",
"loggers": ["RestrictedPython"],
"quality_scale": "internal",
"requirements": ["RestrictedPython==6.1"]
"requirements": ["RestrictedPython==6.2"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.32.3"]
"requirements": ["python-roborock==0.33.2"]
}

View File

@ -51,6 +51,8 @@ class SomaTilt(SomaEntity, CoverEntity):
| CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
CLOSED_UP_THRESHOLD = 80
CLOSED_DOWN_THRESHOLD = 20
@property
def current_cover_tilt_position(self) -> int:
@ -60,7 +62,12 @@ class SomaTilt(SomaEntity, CoverEntity):
@property
def is_closed(self) -> bool:
"""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:
"""Close the cover tilt."""

View File

@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"quality_scale": "silver",
"requirements": ["systembridgeconnector==3.4.9"],
"requirements": ["systembridgeconnector==3.8.2"],
"zeroconf": ["_system-bridge._tcp.local."]
}

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["hatasmota"],
"mqtt": ["tasmota/discovery/#"],
"requirements": ["HATasmota==0.7.1"]
"requirements": ["HATasmota==0.7.3"]
}

View File

@ -88,12 +88,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
hc.SENSOR_CURRENT: {
ICON: "mdi:alpha-a-circle-outline",
DEVICE_CLASS: SensorDeviceClass.CURRENT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_CURRENTNEUTRAL: {
ICON: "mdi:alpha-a-circle-outline",
DEVICE_CLASS: SensorDeviceClass.CURRENT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@ -103,11 +101,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_DISTANCE: {
ICON: "mdi:leak",
DEVICE_CLASS: SensorDeviceClass.DISTANCE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
hc.SENSOR_ENERGY: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_FREQUENCY: {
DEVICE_CLASS: SensorDeviceClass.FREQUENCY,
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_LINK_COUNT: {ICON: "mdi:counter"},
hc.SENSOR_MOISTURE: {
DEVICE_CLASS: SensorDeviceClass.MOISTURE,
ICON: "mdi:cup-water",
},
hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE},
hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
hc.SENSOR_PB0_3: {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,
},
hc.SENSOR_POWERFACTOR: {
ICON: "mdi:alpha-f-circle-outline",
DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@ -162,7 +159,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
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_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
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_TVOC: {ICON: "mdi:air-filter"},
hc.SENSOR_VOLTAGE: {
ICON: "mdi:alpha-v-circle-outline",
DEVICE_CLASS: SensorDeviceClass.VOLTAGE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_WEIGHT: {
ICON: "mdi:scale",
DEVICE_CLASS: SensorDeviceClass.WEIGHT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@ -220,7 +216,6 @@ SENSOR_UNIT_MAP = {
hc.LIGHT_LUX: LIGHT_LUX,
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
hc.PERCENTAGE: PERCENTAGE,
hc.POWER_FACTOR: None,
hc.POWER_WATT: UnitOfPower.WATT,
hc.PRESSURE_HPA: UnitOfPressure.HPA,
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,

View File

@ -23,8 +23,7 @@ class TriggerEntity(TriggerBaseEntity, CoordinatorEntity[TriggerUpdateCoordinato
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
await TriggerBaseEntity.async_added_to_hass(self)
await CoordinatorEntity.async_added_to_hass(self) # type: ignore[arg-type]
await super().async_added_to_hass()
if self.coordinator.data is not None:
self._process_data()

View File

@ -139,7 +139,7 @@ class UnifiEntityTrackerDescriptionMixin(Generic[HandlerT, ApiItemT]):
"""Device tracker local functions."""
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]
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)
@property
def ip_address(self) -> str:
def ip_address(self) -> str | None:
"""Return the primary ip address of the device."""
return self.entity_description.ip_address_fn(self.controller.api, self._obj_id)

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==61"],
"requirements": ["aiounifi==62"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/vodafone_station",
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"requirements": ["aiovodafone==0.1.0"]
"requirements": ["aiovodafone==0.2.0"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
"iot_class": "cloud_polling",
"loggers": ["pywaze", "homeassistant.helpers.location"],
"requirements": ["pywaze==0.3.0"]
"requirements": ["pywaze==0.4.0"]
}

View File

@ -1,5 +1,6 @@
"""Support for Zigbee Home Automation devices."""
import asyncio
import contextlib
import copy
import logging
import os
@ -33,13 +34,16 @@ from .core.const import (
CONF_ZIGPY,
DATA_ZHA,
DATA_ZHA_CONFIG,
DATA_ZHA_DEVICE_TRIGGER_CACHE,
DATA_ZHA_GATEWAY,
DOMAIN,
PLATFORMS,
SIGNAL_ADD_ENTITIES,
RadioType,
)
from .core.device import get_device_automation_triggers
from .core.discovery import GROUP_PROBE
from .radio_manager import ZhaRadioManager
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string})
ZHA_CONFIG_SCHEMA = {
@ -134,10 +138,44 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
else:
_LOGGER.debug("ZHA storage file does not exist or was already removed")
# Re-use the gateway object between ZHA reloads
if (zha_gateway := zha_data.get(DATA_ZHA_GATEWAY)) is None:
# Load and cache device trigger information early
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)
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:
await zha_gateway.async_initialize()
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)
config_entry.async_on_unload(zha_gateway.shutdown)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))},

View File

@ -186,6 +186,7 @@ DATA_ZHA = "zha"
DATA_ZHA_CONFIG = "config"
DATA_ZHA_BRIDGE_ID = "zha_bridge_id"
DATA_ZHA_CORE_EVENTS = "zha_core_events"
DATA_ZHA_DEVICE_TRIGGER_CACHE = "zha_device_trigger_cache"
DATA_ZHA_GATEWAY = "zha_gateway"
DEBUG_COMP_BELLOWS = "bellows"

View File

@ -93,6 +93,16 @@ _UPDATE_ALIVE_INTERVAL = (60, 90)
_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):
"""Status of a device."""
@ -311,16 +321,7 @@ class ZHADevice(LogMixin):
@cached_property
def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]:
"""Return the device automation triggers for this device."""
triggers = {
("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
return get_device_automation_triggers(self._zigpy_device)
@property
def available_signal(self) -> str:

View File

@ -149,12 +149,6 @@ class ZHAGateway:
self.config_entry = config_entry
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]:
"""Get an uninitialized instance of a zigpy `ControllerApplication`."""
radio_type = self.config_entry.data[CONF_RADIO_TYPE]
@ -197,6 +191,12 @@ class ZHAGateway:
async def async_initialize(self) -> None:
"""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()
self.application_controller = await app_controller_cls.new(
config=app_config,
@ -204,23 +204,6 @@ class ZHAGateway:
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):
try:
await self.application_controller.startup(auto_form=True)
@ -242,13 +225,14 @@ class ZHAGateway:
else:
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._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
if not loaded_groups:
self.async_load_devices()
self.async_load_groups()
self.application_controller.add_listener(self)
@ -766,6 +750,14 @@ class ZHAGateway:
unsubscribe()
for device in self.devices.values():
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()
def handle_message(

View File

@ -9,12 +9,12 @@ from homeassistant.components.device_automation.exceptions import (
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.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.typing import ConfigType
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
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(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate 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])
try:
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
):
if trigger not in triggers:
raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}")
return config
@ -53,26 +64,26 @@ async def async_attach_trigger(
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE])
try:
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
except (KeyError, AttributeError) as err:
ieee, triggers = _get_device_trigger_data(hass, config[CONF_DEVICE_ID])
except KeyError as err:
raise HomeAssistantError(
f"Unable to get zha device {config[CONF_DEVICE_ID]}"
) 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}")
trigger = zha_device.device_automation_triggers[trigger_key]
event_config = {
event_config = event_trigger.TRIGGER_SCHEMA(
{
event_trigger.CONF_PLATFORM: "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(
hass, event_config, action, trigger_info, platform_type="device"
)
@ -83,17 +94,14 @@ async def async_get_triggers(
) -> list[dict[str, str]]:
"""List device triggers.
Make sure the device supports device automations and
if it does return the trigger list.
Make sure the device supports device automations and 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 []
triggers = []
for trigger, subtype in zha_device.device_automation_triggers:
triggers.append(
return [
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: ZHA_DOMAIN,
@ -101,6 +109,5 @@ async def async_get_triggers(
CONF_TYPE: trigger,
CONF_SUBTYPE: subtype,
}
)
return triggers
for trigger, subtype in triggers
]

View File

@ -25,9 +25,9 @@
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.103",
"zigpy-deconz==0.21.0",
"zigpy-deconz==0.21.1",
"zigpy==0.57.1",
"zigpy-xbee==0.18.1",
"zigpy-xbee==0.18.2",
"zigpy-zigate==0.11.0",
"zigpy-znp==0.11.4",
"universal-silabs-flasher==0.0.13"

View File

@ -8,7 +8,7 @@ import copy
import enum
import logging
import os
from typing import Any
from typing import Any, Self
from bellows.config import CONF_USE_THREAD
import voluptuous as vol
@ -127,8 +127,21 @@ class ZhaRadioManager:
self.backups: list[zigpy.backups.NetworkBackup] = []
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
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."""
assert self.radio_type is not None
@ -155,10 +168,9 @@ class ZhaRadioManager:
)
try:
await app.connect()
yield app
finally:
await app.disconnect()
await app.shutdown()
await asyncio.sleep(CONNECT_DELAY_S)
async def restore_backup(
@ -170,7 +182,8 @@ class ZhaRadioManager:
):
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)
@staticmethod
@ -218,7 +231,9 @@ class ZhaRadioManager:
"""Connect to the radio and load its current network settings."""
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
try:
await app.load_network_info()
@ -241,12 +256,14 @@ class ZhaRadioManager:
async def async_form_network(self) -> None:
"""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()
async def async_reset_adapter(self) -> None:
"""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()
async def async_restore_backup_step_1(self) -> bool:

View File

@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["zwave_js_server"],
"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": [
{
"vid": "0658",

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import voluptuous as vol
from zwave_js_server.model.node import Node
from homeassistant import data_entry_flow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
@ -14,10 +13,10 @@ from .helpers import async_get_node_from_device_id
class DeviceConfigFileChangedFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, node: Node, device_name: str) -> None:
def __init__(self, data: dict[str, str]) -> None:
"""Initialize."""
self.node = node
self.device_name = device_name
self.device_name: str = data["device_name"]
self.device_id: str = data["device_id"]
async def async_step_init(
self, user_input: dict[str, str] | None = None
@ -30,7 +29,14 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
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_show_form(
@ -41,15 +47,11 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str] | None,
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
) -> RepairsFlow:
"""Create flow."""
if issue_id.split(".")[0] == "device_config_file_changed":
assert data
return DeviceConfigFileChangedFlow(
async_get_node_from_device_id(hass, data["device_id"]), data["device_name"]
)
return DeviceConfigFileChangedFlow(data)
return ConfirmRepairFlow()

View File

@ -170,6 +170,9 @@
"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."
}
},
"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."
}
}
}

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -9,9 +9,9 @@ attrs==23.1.0
awesomeversion==22.9.0
bcrypt==4.0.1
bleak-retry-connector==3.1.3
bleak==0.21.0
bleak==0.21.1
bluetooth-adapters==0.16.1
bluetooth-auto-recovery==1.2.2
bluetooth-auto-recovery==1.2.3
bluetooth-data-tools==1.11.0
certifi>=2021.5.30
ciso8601==2.3.0
@ -19,10 +19,10 @@ cryptography==41.0.3
dbus-fast==1.95.2
fnv-hash-fast==0.4.1
ha-av==10.1.1
hass-nabucasa==0.70.0
hass-nabucasa==0.71.0
hassil==1.2.5
home-assistant-bluetooth==1.10.3
home-assistant-frontend==20230908.0
home-assistant-frontend==20230911.0
home-assistant-intents==2023.8.2
httpx==0.24.1
ifaddr==0.2.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.9.1"
version = "2023.9.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -29,7 +29,7 @@ DoorBirdPy==2.1.0
HAP-python==4.7.1
# homeassistant.components.tasmota
HATasmota==0.7.1
HATasmota==0.7.3
# homeassistant.components.mastodon
Mastodon.py==1.5.1
@ -121,7 +121,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.0.3
# homeassistant.components.python_script
RestrictedPython==6.1
RestrictedPython==6.2
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@ -363,13 +363,13 @@ aiosyncthing==0.5.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==61
aiounifi==62
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
# homeassistant.components.vodafone_station
aiovodafone==0.1.0
aiovodafone==0.2.0
# homeassistant.components.waqi
aiowaqi==0.2.1
@ -521,7 +521,7 @@ bizkaibus==0.1.1
bleak-retry-connector==3.1.3
# homeassistant.components.bluetooth
bleak==0.21.0
bleak==0.21.1
# homeassistant.components.blebox
blebox-uniapi==2.1.4
@ -543,7 +543,7 @@ bluemaestro-ble==0.2.3
bluetooth-adapters==0.16.1
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.2.2
bluetooth-auto-recovery==1.2.3
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@ -958,7 +958,7 @@ ha-philipsjs==3.1.0
habitipy==0.2.0
# homeassistant.components.cloud
hass-nabucasa==0.70.0
hass-nabucasa==0.71.0
# homeassistant.components.splunk
hass-splunk==0.1.1
@ -994,7 +994,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20230908.0
home-assistant-frontend==20230911.0
# homeassistant.components.conversation
home-assistant-intents==2023.8.2
@ -1438,7 +1438,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.31.9
plugwise==0.32.2
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@ -1851,7 +1851,7 @@ pymitv==1.4.3
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.5.1
pymodbus==3.5.2
# homeassistant.components.monoprice
pymonoprice==0.4
@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==0.32.3
python-roborock==0.33.2
# homeassistant.components.smarttub
python-smarttub==0.0.33
@ -2231,7 +2231,7 @@ pyvlx==0.2.20
pyvolumio==0.1.5
# homeassistant.components.waze_travel_time
pywaze==0.3.0
pywaze==0.4.0
# homeassistant.components.html5
pywebpush==1.9.2
@ -2504,7 +2504,7 @@ swisshydrodata==0.1.0
synology-srm==0.2.0
# homeassistant.components.system_bridge
systembridgeconnector==3.4.9
systembridgeconnector==3.8.2
# homeassistant.components.tailscale
tailscale==0.2.0
@ -2603,7 +2603,7 @@ twitchAPI==3.10.0
uasiren==0.0.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.1
ultraheat-api==0.5.7
# homeassistant.components.unifiprotect
unifi-discovery==1.1.7
@ -2781,10 +2781,10 @@ zhong-hong-hvac==1.0.9
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha
zigpy-deconz==0.21.0
zigpy-deconz==0.21.1
# homeassistant.components.zha
zigpy-xbee==0.18.1
zigpy-xbee==0.18.2
# homeassistant.components.zha
zigpy-zigate==0.11.0
@ -2799,7 +2799,7 @@ zigpy==0.57.1
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.51.1
zwave-js-server-python==0.51.2
# homeassistant.components.zwave_me
zwave-me-ws==0.4.3

View File

@ -28,7 +28,7 @@ DoorBirdPy==2.1.0
HAP-python==4.7.1
# homeassistant.components.tasmota
HATasmota==0.7.1
HATasmota==0.7.3
# homeassistant.components.doods
# homeassistant.components.generic
@ -108,7 +108,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.0.3
# homeassistant.components.python_script
RestrictedPython==6.1
RestrictedPython==6.2
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@ -338,13 +338,13 @@ aiosyncthing==0.5.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==61
aiounifi==62
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
# homeassistant.components.vodafone_station
aiovodafone==0.1.0
aiovodafone==0.2.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -439,7 +439,7 @@ bimmer-connected==0.14.0
bleak-retry-connector==3.1.3
# homeassistant.components.bluetooth
bleak==0.21.0
bleak==0.21.1
# homeassistant.components.blebox
blebox-uniapi==2.1.4
@ -454,7 +454,7 @@ bluemaestro-ble==0.2.3
bluetooth-adapters==0.16.1
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.2.2
bluetooth-auto-recovery==1.2.3
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@ -753,7 +753,7 @@ ha-philipsjs==3.1.0
habitipy==0.2.0
# homeassistant.components.cloud
hass-nabucasa==0.70.0
hass-nabucasa==0.71.0
# homeassistant.components.conversation
hassil==1.2.5
@ -777,7 +777,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20230908.0
home-assistant-frontend==20230911.0
# homeassistant.components.conversation
home-assistant-intents==2023.8.2
@ -1086,7 +1086,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.31.9
plugwise==0.32.2
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.5.1
pymodbus==3.5.2
# homeassistant.components.monoprice
pymonoprice==0.4
@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3
# homeassistant.components.roborock
python-roborock==0.32.3
python-roborock==0.33.2
# homeassistant.components.smarttub
python-smarttub==0.0.33
@ -1639,7 +1639,7 @@ pyvizio==0.1.61
pyvolumio==0.1.5
# homeassistant.components.waze_travel_time
pywaze==0.3.0
pywaze==0.4.0
# homeassistant.components.html5
pywebpush==1.9.2
@ -1837,7 +1837,7 @@ sunwatcher==0.2.1
surepy==0.8.0
# homeassistant.components.system_bridge
systembridgeconnector==3.4.9
systembridgeconnector==3.8.2
# homeassistant.components.tailscale
tailscale==0.2.0
@ -1903,7 +1903,7 @@ twitchAPI==3.10.0
uasiren==0.0.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.1
ultraheat-api==0.5.7
# homeassistant.components.unifiprotect
unifi-discovery==1.1.7
@ -2045,10 +2045,10 @@ zeversolar==0.3.1
zha-quirks==0.0.103
# homeassistant.components.zha
zigpy-deconz==0.21.0
zigpy-deconz==0.21.1
# homeassistant.components.zha
zigpy-xbee==0.18.1
zigpy-xbee==0.18.2
# homeassistant.components.zha
zigpy-zigate==0.11.0
@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4
zigpy==0.57.1
# homeassistant.components.zwave_js
zwave-js-server-python==0.51.1
zwave-js-server-python==0.51.2
# homeassistant.components.zwave_me
zwave-me-ws==0.4.3

View File

@ -5,8 +5,11 @@ from unittest.mock import patch
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from homeassistant.components.airthings_ble.const import DOMAIN
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
@ -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(
name="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,
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_data={},
service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
source="local",
device=generate_ble_device(
"cc:cc:cc:cc:cc:cc",
"cc-cc-cc-cc-cc-cc",
service_data={
# Sensor data
"b42e2a68-ade7-11e4-89d3-123b93f75cba": bytearray(
b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A"
),
# 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(
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
@ -99,3 +136,62 @@ WAVE_DEVICE_INFO = AirthingsDevice(
},
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

View 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

View File

@ -32,7 +32,6 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
"relayer_server": "test-relayer-server",
"accounts_server": "test-acounts-server",
"cloudhook_server": "test-cloudhook-server",
"remote_sni_server": "test-remote-sni-server",
"alexa_server": "test-alexa-server",
"acme_server": "test-acme-server",
"remotestate_server": "test-remotestate-server",

View File

@ -8,11 +8,25 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
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.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(
hass: HomeAssistant,
@ -41,10 +55,8 @@ async def test_update_available(
) -> None:
"""Test update entities."""
with patch(
"homeassistant.components.fritz.common.FritzBoxTools._update_device_info",
return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL),
):
fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE})
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
@ -90,10 +102,9 @@ async def test_available_update_can_be_installed(
) -> None:
"""Test update entities."""
fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE})
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",
return_value=True,
) as mocked_update_call:

View File

@ -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(
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:

View File

@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
with patch(
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
), patch(
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
return_value=mock_connect_app,
), patch(
"homeassistant.components.zha.async_setup_entry",

View File

@ -25,7 +25,7 @@ def mock_zha():
)
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,
), patch(
"homeassistant.components.zha.async_setup_entry",

View File

@ -45,7 +45,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
with patch(
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
), patch(
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
return_value=mock_connect_app,
):
yield

View File

@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
with patch(
"bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe
), patch(
"homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app",
"homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app",
return_value=mock_connect_app,
), patch(
"homeassistant.components.zha.async_setup_entry",

View 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]
}

View File

@ -1,6 +1,7 @@
"""Tests for the IPP config flow."""
import dataclasses
from unittest.mock import MagicMock
import json
from unittest.mock import MagicMock, patch
from pyipp import (
IPPConnectionError,
@ -8,6 +9,7 @@ from pyipp import (
IPPError,
IPPParseError,
IPPVersionNotSupportedError,
Printer,
)
import pytest
@ -23,7 +25,7 @@ from . import (
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, load_fixture
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"
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(
hass: HomeAssistant,
mock_ipp_config_flow: MagicMock,
@ -337,6 +364,21 @@ async def test_zeroconf_empty_unique_id(
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(
hass: HomeAssistant,
@ -355,6 +397,21 @@ async def test_zeroconf_no_unique_id(
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(
hass: HomeAssistant,
@ -448,3 +505,45 @@ async def test_full_zeroconf_tls_flow_implementation(
assert result["result"]
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"

View File

@ -149,7 +149,7 @@ async def mock_do_cycle_fixture(
mock_pymodbus_return,
) -> FrozenDateTimeFactory:
"""Trigger update call with time_changed event."""
freezer.tick(timedelta(seconds=90))
freezer.tick(timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
return freezer

View File

@ -1,4 +1,6 @@
"""The tests for the Modbus sensor component."""
import struct
from freezegun.api import FrozenDateTimeFactory
import pytest
@ -267,7 +269,6 @@ async def test_config_wrong_struct_sensor(
{
CONF_NAME: TEST_ENTITY_NAME,
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(
("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,
@ -710,7 +726,6 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 51,
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
@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(
"do_config",
[
@ -918,27 +992,23 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
],
)
@pytest.mark.parametrize(
("register_words", "do_exception", "start_expect", "end_expect"),
("register_words", "do_exception"),
[
(
[0x8000],
True,
"17",
STATE_UNAVAILABLE,
),
],
)
async def test_lazy_error_sensor(
hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory, start_expect, end_expect
hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory
) -> None:
"""Run test for sensor."""
hass.states.async_set(ENTITY_ID, 17)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == start_expect
await do_next_cycle(hass, mock_do_cycle, 11)
assert hass.states.get(ENTITY_ID).state == start_expect
await do_next_cycle(hass, mock_do_cycle, 11)
assert hass.states.get(ENTITY_ID).state == end_expect
assert hass.states.get(ENTITY_ID).state == "17"
await do_next_cycle(hass, mock_do_cycle, 5)
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
@pytest.mark.parametrize(
@ -965,10 +1035,35 @@ async def test_lazy_error_sensor(
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_STRUCTURE: ">4f",
},
# floats: 7.931250095367432, 10.600000381469727,
# floats: nan, 10.600000381469727,
# 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],
"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:
@ -1003,7 +1110,6 @@ async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> No
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 201,
CONF_SCAN_INTERVAL: 1,
},
],
},

View File

@ -7,6 +7,7 @@ import pytest
from homeassistant.components import mqtt, sensor
from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
EVENT_HOMEASSISTANT_STARTED,
EVENT_STATE_CHANGED,
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.
"""
# mqtt_mock = await mqtt_mock_entry()
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
hass.state = CoreState.starting
@ -352,3 +352,61 @@ async def test_default_entity_and_device_name(
# Assert that an issues ware registered
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

View File

@ -20,6 +20,12 @@
"setpoint": 13.0,
"temperature": 24.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,
@ -43,6 +49,12 @@
"temperature_difference": 2.0,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A07"
},
@ -60,6 +72,12 @@
"temperature_difference": 1.7,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A05"
},
@ -99,6 +117,12 @@
"setpoint": 13.0,
"temperature": 30.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,
@ -122,6 +146,12 @@
"temperature_difference": 1.8,
"valve_position": 100
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A09"
},
@ -145,6 +175,12 @@
"setpoint": 13.0,
"temperature": 30.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,
@ -187,6 +223,12 @@
"temperature_difference": 1.9,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A04"
},
@ -246,6 +288,12 @@
"setpoint": 9.0,
"temperature": 27.4
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.0,
"upper_bound": 2.0
},
"thermostat": {
"lower_bound": 4.0,
"resolution": 0.01,

View File

@ -95,6 +95,12 @@
"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"
},
@ -123,6 +129,12 @@
"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,
@ -200,6 +212,12 @@
"temperature_difference": -0.2,
"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": "ABCD012345670A09"
},
@ -217,6 +235,12 @@
"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"
},
@ -245,6 +269,12 @@
"setpoint": 21.5,
"temperature": 20.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,
@ -289,6 +319,12 @@
"temperature_difference": 0.1,
"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": "ABCD012345670A10"
},
@ -317,6 +353,12 @@
"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,
@ -353,6 +395,12 @@
"temperature_difference": 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,
@ -387,6 +435,12 @@
"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,

View File

@ -76,6 +76,12 @@
"setpoint": 20.5,
"temperature": 19.3
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": -0.5,
"upper_bound": 2.0
},
"thermostat": {
"lower_bound": 4.0,
"resolution": 0.1,

View File

@ -40,6 +40,12 @@
"temperature_difference": 2.3,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A01"
},
@ -118,6 +124,12 @@
"setpoint_low": 20.0,
"temperature": 239
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.0,
"upper_bound": 2.0
},
"thermostat": {
"lower_bound": 0.0,
"resolution": 0.01,

View File

@ -45,6 +45,12 @@
"temperature_difference": 2.3,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A01"
},
@ -114,6 +120,12 @@
"setpoint": 15.0,
"temperature": 17.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,

View File

@ -78,6 +78,12 @@
"setpoint_low": 20.5,
"temperature": 26.3
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": -0.5,
"upper_bound": 2.0
},
"thermostat": {
"lower_bound": 4.0,
"resolution": 0.1,

View File

@ -78,6 +78,12 @@
"setpoint_low": 20.5,
"temperature": 23.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": -0.5,
"upper_bound": 2.0
},
"thermostat": {
"lower_bound": 4.0,
"resolution": 0.1,

View File

@ -2,7 +2,7 @@
"devices": {
"03e65b16e4b247a29ae0d75a78cb492e": {
"binary_sensors": {
"plugwise_notification": false
"plugwise_notification": true
},
"dev_class": "gateway",
"firmware": "4.4.2",
@ -51,7 +51,11 @@
},
"gateway": {
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
"notifications": {},
"notifications": {
"97a04c0c263049b29350a660b4cdd01e": {
"warning": "The Smile P1 is not connected to a smart meter."
}
},
"smile_name": "Smile P1"
}
}

View File

@ -1 +1,5 @@
{}
{
"97a04c0c263049b29350a660b4cdd01e": {
"warning": "The Smile P1 is not connected to a smart meter."
}
}

View File

@ -48,15 +48,6 @@
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A07"
},
"71e1944f2a944b26ad73323e399efef0": {
"dev_class": "switching",
"members": ["5ca521ac179d468e91d772eeeb8a2117"],
"model": "Switchgroup",
"name": "Test",
"switches": {
"relay": true
}
},
"aac7b735042c4832ac9ff33aae4f453b": {
"dev_class": "dishwasher",
"firmware": "2011-06-27T10:52:18+02:00",

View File

@ -31,159 +31,141 @@ async def test_diagnostics(
},
},
"devices": {
"df4a4a8169904cdb9c03d61a21f42140": {
"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",
"02cf28bfec924855854c544690a609ef": {
"available": True,
"dev_class": "vcr",
"firmware": "2019-06-21T02:00:00+02:00",
"location": "cd143c07248f491493cea0533bc3d669",
"model": "Plug",
"name": "NVR",
"sensors": {
"temperature": 26.0,
"setpoint": 21.5,
"temperature_difference": 3.5,
"valve_position": 100,
"electricity_consumed": 34.0,
"electricity_consumed_interval": 9.15,
"electricity_produced": 0.0,
"electricity_produced_interval": 0.0,
},
},
"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",
"switches": {"lock": True, "relay": True},
"vendor": "Plugwise",
"available": True,
"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,
},
"zigbee_mac_address": "ABCD012345670A15",
},
"21f2b542c49845e6bb416884c55778d6": {
"available": True,
"dev_class": "game_console",
"firmware": "2019-06-21T02:00:00+02:00",
"location": "cd143c07248f491493cea0533bc3d669",
"model": "Plug",
"name": "Playstation Smart Plug",
"zigbee_mac_address": "ABCD012345670A12",
"vendor": "Plugwise",
"available": True,
"sensors": {
"electricity_consumed": 82.6,
"electricity_consumed_interval": 8.6,
"electricity_produced": 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": {
"available": True,
"dev_class": "central_heating_pump",
"firmware": "2019-06-21T02:00:00+02:00",
"location": "c50f167537524366a5af7aa3942feb1e",
"model": "Plug",
"name": "CV Pomp",
"zigbee_mac_address": "ABCD012345670A05",
"vendor": "Plugwise",
"available": True,
"sensors": {
"electricity_consumed": 35.6,
"electricity_consumed_interval": 7.37,
@ -191,153 +173,88 @@ async def test_diagnostics(
"electricity_produced_interval": 0.0,
},
"switches": {"relay": True},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A05",
},
"90986d591dcd426cae3ec3e8111ff730": {
"binary_sensors": {"heating_state": True},
"dev_class": "heater_central",
"location": "1f9dcf83fd4e4b66b72ff787957bfe5d",
"model": "Unknown",
"name": "OnOff",
"binary_sensors": {"heating_state": True},
"sensors": {
"water_temperature": 70.0,
"intended_boiler_temperature": 70.0,
"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": {
"available": True,
"dev_class": "settop",
"firmware": "2019-06-21T02:00:00+02:00",
"location": "cd143c07248f491493cea0533bc3d669",
"model": "Plug",
"name": "Fibaro HC2",
"zigbee_mac_address": "ABCD012345670A13",
"vendor": "Plugwise",
"available": True,
"sensors": {
"electricity_consumed": 12.5,
"electricity_consumed_interval": 3.8,
"electricity_produced": 0.0,
"electricity_produced_interval": 0.0,
},
"switches": {"relay": True, "lock": 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",
"switches": {"lock": True, "relay": True},
"vendor": "Plugwise",
"thermostat": {
"setpoint": 15.0,
"lower_bound": 0.0,
"upper_bound": 99.9,
"resolution": 0.01,
"zigbee_mac_address": "ABCD012345670A13",
},
"a2c3583e0a6349358998b760cea82d2a": {
"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",
"firmware": "2019-03-27T01:00:00+01:00",
"hardware": "1",
"location": "08963fec7c53423ca5680aa4cb502c63",
"location": "12493538af164a409c6a1c79e38afe1c",
"model": "Tom/Floor",
"name": "Thermostatic Radiator Badkamer",
"zigbee_mac_address": "ABCD012345670A17",
"vendor": "Plugwise",
"available": True,
"name": "Bios Cv Thermostatic Radiator ",
"sensors": {
"temperature": 19.1,
"setpoint": 14.0,
"battery": 51,
"temperature_difference": -0.4,
"battery": 62,
"setpoint": 13.0,
"temperature": 17.2,
"temperature_difference": -0.2,
"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",
"thermostat": {
"setpoint": 14.0,
"lower_bound": 0.0,
"upper_bound": 99.9,
"resolution": 0.01,
"zigbee_mac_address": "ABCD012345670A09",
},
"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,
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
"active_preset": "away",
"available_schedules": [
"CV Roan",
"Bios Schema met Film Avond",
@ -345,46 +262,76 @@ async def test_diagnostics(
"Badkamer Schema",
"CV Jessie",
],
"select_schedule": "Badkamer Schema",
"last_used": "Badkamer Schema",
"dev_class": "zone_thermostat",
"firmware": "2016-08-02T02:00:00+02:00",
"hardware": "255",
"last_used": "GF7 Woonkamer",
"location": "c50f167537524366a5af7aa3942feb1e",
"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": {
"dev_class": "router",
"thermostat": {
"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",
"location": "cd143c07248f491493cea0533bc3d669",
"model": "Plug",
"name": "Ziggo Modem",
"zigbee_mac_address": "ABCD012345670A01",
"vendor": "Plugwise",
"available": True,
"name": "NAS",
"sensors": {
"electricity_consumed": 12.2,
"electricity_consumed_interval": 2.97,
"electricity_consumed": 16.5,
"electricity_consumed_interval": 0.5,
"electricity_produced": 0.0,
"electricity_produced_interval": 0.0,
},
"switches": {"relay": True, "lock": True},
"switches": {"lock": True, "relay": True},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A14",
},
"e7693eb9582644e5b865dba8d4447cf1": {
"dev_class": "thermostatic_radiator_valve",
"d3da73bde12a47d5a6b8f9dad971f2ec": {
"available": True,
"dev_class": "thermo_sensor",
"firmware": "2019-03-27T01:00:00+01:00",
"hardware": "1",
"location": "446ac08dd04d4eff8ac57489757b7314",
"location": "82fa13f017d240daa0d0ea1775420f24",
"model": "Tom/Floor",
"name": "CV Kraan Garage",
"zigbee_mac_address": "ABCD012345670A11",
"vendor": "Plugwise",
"thermostat": {
"setpoint": 5.5,
"lower_bound": 0.0,
"upper_bound": 100.0,
"resolution": 0.01,
"name": "Thermostatic Radiator Jessie",
"sensors": {
"battery": 62,
"setpoint": 15.0,
"temperature": 17.1,
"temperature_difference": 0.1,
"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": "ABCD012345670A10",
},
"df4a4a8169904cdb9c03d61a21f42140": {
"active_preset": "away",
"available": True,
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
"active_preset": "no_frost",
"available_schedules": [
"CV Roan",
"Bios Schema met Film Avond",
@ -392,16 +339,123 @@ async def test_diagnostics(
"Badkamer Schema",
"CV Jessie",
],
"select_schedule": "None",
"dev_class": "zone_thermostat",
"firmware": "2016-10-27T02:00:00+02:00",
"hardware": "255",
"last_used": "Badkamer Schema",
"location": "12493538af164a409c6a1c79e38afe1c",
"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": {
"temperature": 15.6,
"setpoint": 5.5,
"battery": 68,
"setpoint": 5.5,
"temperature": 15.6,
"temperature_difference": 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",
},
},
}

View File

@ -38,7 +38,7 @@ async def test_anna_max_boiler_temp_change(
assert mock_smile_anna.set_number_setpoint.call_count == 1
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
mock_smile_adam_2.set_number_setpoint.assert_called_with(
"max_dhw_temperature", 55.0
"max_dhw_temperature", "056ee145a816487eaa69243c3280f8bf", 55.0
)

View File

@ -1835,3 +1835,35 @@ async def test_entity_id_update_discovery_update(
await help_test_entity_id_update_discovery_update(
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"

View File

@ -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 = {
"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(
@ -409,6 +542,87 @@ async def test_controlling_state_via_mqtt(
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(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
) -> None:

View File

@ -283,3 +283,35 @@ async def test_entity_id_update_discovery_update(
await help_test_entity_id_update_discovery_update(
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"

View File

@ -1,7 +1,7 @@
"""The test for the Template sensor platform."""
from asyncio import Event
from datetime import timedelta
from unittest.mock import patch
from unittest.mock import ANY, patch
import pytest
@ -1140,6 +1140,48 @@ async def test_trigger_entity(
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(
"config",

View File

@ -293,14 +293,20 @@ def zigpy_device_mock(zigpy_app_controller):
return _mock_dev
@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True))
@pytest.fixture
def zha_device_joined(hass, setup_zha):
"""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()
await setup_zha()
if setup_zha:
await setup_zha_fixture()
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 hass.async_block_till_done()
return zha_gateway.get_device(zigpy_dev.ieee)
@ -308,17 +314,21 @@ def zha_device_joined(hass, setup_zha):
return _zha_device
@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True))
@pytest.fixture
def zha_device_restored(hass, zigpy_app_controller, setup_zha):
"""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
if last_seen is not None:
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]
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))
):
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

View File

@ -62,13 +62,6 @@ def mock_multipan_platform():
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)
def mock_app():
"""Mock zigpy app interface."""

View File

@ -9,6 +9,9 @@ import zigpy.zcl.clusters.general as general
import homeassistant.components.automation as automation
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.const import Platform
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 tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_get_device_automations,
async_mock_service,
@ -45,6 +49,16 @@ LONG_PRESS = "remote_button_long_press"
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)
def sensor_platforms_only():
"""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):
"""IAS device fixture."""
zigpy_device = zigpy_device_mock(
{
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,
}
}
)
zigpy_device = zigpy_device_mock(SWITCH_SIGNATURE)
zha_device = await zha_device_joined_restored(zigpy_device)
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: "
"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

View File

@ -6,7 +6,6 @@ import pytest
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from zigpy.exceptions import TransientConnectionError
from homeassistant.components.zha import async_setup_entry
from homeassistant.components.zha.core.const import (
CONF_BAUDRATE,
CONF_RADIO_TYPE,
@ -22,7 +21,7 @@ from .test_light import LIGHT_ON_OFF
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"
@ -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)
)
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:
"""Test migration of config entry from v3, applying corrections to the port path."""
config_entry_v3 = MockConfigEntry(
@ -150,14 +149,9 @@ async def test_setup_with_v3_cleaning_uri(
)
config_entry_v3.add_to_hass(hass)
with patch(
"homeassistant.components.zha.ZHAGateway", return_value=AsyncMock()
) as mock_gateway:
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
await hass.config_entries.async_setup(config_entry_v3.entry_id)
await hass.async_block_till_done()
await hass.config_entries.async_unload(config_entry_v3.entry_id)
assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path

View File

@ -32,9 +32,7 @@ def disable_platform_only():
@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.0001
), patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001):
with patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001):
yield
@ -99,7 +97,7 @@ def mock_connect_zigpy_app() -> Generator[MagicMock, None, None]:
)
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,
):
yield mock_connect_app

View File

@ -22,16 +22,10 @@ import homeassistant.helpers.issue_registry as ir
from tests.typing import ClientSessionGenerator, WebSocketGenerator
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)
async def _trigger_repair_issue(
hass: HomeAssistant, client, multisensor_6_state
) -> Node:
"""Trigger repair issue."""
# Create a node
node_state = deepcopy(multisensor_6_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()
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)})
assert device
issue_id = f"device_config_file_changed.{device.id}"
@ -157,3 +168,46 @@ async def test_invalid_issue(
msg = await ws_client.receive_json()
assert msg["success"]
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}