This commit is contained in:
Franck Nijhof 2023-11-10 12:41:51 +01:00 committed by GitHub
commit a3319262ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 665 additions and 255 deletions

View File

@ -9,5 +9,5 @@
}, },
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"], "loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==3.3.0"] "requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.3.1"] "requirements": ["aioairzone-cloud==0.3.5"]
} }

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink", "documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["blinkpy"], "loggers": ["blinkpy"],
"requirements": ["blinkpy==0.22.2"] "requirements": ["blinkpy==0.22.3"]
} }

View File

@ -18,7 +18,7 @@
"bleak-retry-connector==3.3.0", "bleak-retry-connector==3.3.0",
"bluetooth-adapters==0.16.1", "bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3", "bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.13.0", "bluetooth-data-tools==1.14.0",
"dbus-fast==2.12.0" "dbus-fast==2.12.0"
] ]
} }

View File

@ -53,6 +53,8 @@ class DSMRConnection:
self._protocol = protocol self._protocol = protocol
self._telegram: dict[str, DSMRObject] = {} self._telegram: dict[str, DSMRObject] = {}
self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER
if dsmr_version == "5B":
self._equipment_identifier = obis_ref.BELGIUM_EQUIPMENT_IDENTIFIER
if dsmr_version == "5L": if dsmr_version == "5L":
self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER
if dsmr_version == "Q3D": if dsmr_version == "Q3D":

View File

@ -34,6 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
DSMR_PROTOCOL = "dsmr_protocol" DSMR_PROTOCOL = "dsmr_protocol"
RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol"
# Temp obis until sensors replaced by mbus variants
BELGIUM_5MIN_GAS_METER_READING = r"\d-\d:24\.2\.3.+?\r\n"

View File

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["dsmr_parser"], "loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.3.0"] "requirements": ["dsmr-parser==1.3.1"]
} }

View File

@ -44,7 +44,6 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import ( from .const import (
BELGIUM_5MIN_GAS_METER_READING,
CONF_DSMR_VERSION, CONF_DSMR_VERSION,
CONF_PRECISION, CONF_PRECISION,
CONF_PROTOCOL, CONF_PROTOCOL,
@ -382,16 +381,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.GAS, device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading",
obis_reference=BELGIUM_5MIN_GAS_METER_READING,
dsmr_versions={"5B"},
is_gas=True,
force_update=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
DSMRSensorEntityDescription( DSMRSensorEntityDescription(
key="gas_meter_reading", key="gas_meter_reading",
translation_key="gas_meter_reading", translation_key="gas_meter_reading",
@ -405,6 +394,31 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
) )
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
"""Return correct entity for 5B Gas meter."""
ref = None
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS2_METER_READING2
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS3_METER_READING2
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS4_METER_READING2
elif ref is None:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
return DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading",
obis_reference=ref,
dsmr_versions={"5B"},
is_gas=True,
force_update=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
@ -438,6 +452,10 @@ async def async_setup_entry(
return (entity_description.device_class, UNIT_CONVERSION[uom]) return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom) return (entity_description.device_class, uom)
all_sensors = SENSORS
if dsmr_version == "5B":
all_sensors += (add_gas_sensor_5B(telegram),)
entities.extend( entities.extend(
[ [
DSMREntity( DSMREntity(
@ -448,7 +466,7 @@ async def async_setup_entry(
telegram, description telegram, description
), # type: ignore[arg-type] ), # type: ignore[arg-type]
) )
for description in SENSORS for description in all_sensors
if ( if (
description.dsmr_versions is None description.dsmr_versions is None
or dsmr_version in description.dsmr_versions or dsmr_version in description.dsmr_versions

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyenphase"], "loggers": ["pyenphase"],
"requirements": ["pyenphase==1.14.1"], "requirements": ["pyenphase==1.14.2"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_enphase-envoy._tcp.local." "type": "_enphase-envoy._tcp.local."

View File

@ -596,6 +596,10 @@ def _async_setup_device_registry(
model = project_name[1] model = project_name[1]
hw_version = device_info.project_version hw_version = device_info.project_version
suggested_area = None
if device_info.suggested_area:
suggested_area = device_info.suggested_area
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_or_create( device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
@ -606,6 +610,7 @@ def _async_setup_device_registry(
model=model, model=model,
sw_version=sw_version, sw_version=sw_version,
hw_version=hw_version, hw_version=hw_version,
suggested_area=suggested_area,
) )
return device_entry.id return device_entry.id

View File

@ -16,8 +16,8 @@
"loggers": ["aioesphomeapi", "noiseprotocol"], "loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [ "requirements": [
"async-interrupt==1.1.1", "async-interrupt==1.1.1",
"aioesphomeapi==18.1.0", "aioesphomeapi==18.2.4",
"bluetooth-data-tools==1.13.0", "bluetooth-data-tools==1.14.0",
"esphome-dashboard-api==1.2.3" "esphome-dashboard-api==1.2.3"
], ],
"zeroconf": ["_esphomelib._tcp.local."] "zeroconf": ["_esphomelib._tcp.local."]

View File

@ -139,9 +139,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
return self._device_information["fwVersion"] return self._device_information["fwVersion"]
@property @property
def serial_number(self) -> str: def serial_number(self) -> str | None:
"""Return the serial number for the device.""" """Return the serial number for the device."""
return self._device_information["serialNumber"] return self._device_information.get("serialNumber")
@property @property
def pending_info_alerts_count(self) -> int: def pending_info_alerts_count(self) -> int:

View File

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

View File

@ -122,12 +122,12 @@ class KNXExposeSensor:
"""Extract value from state.""" """Extract value from state."""
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
value = self.expose_default value = self.expose_default
elif self.expose_attribute is not None:
_attr = state.attributes.get(self.expose_attribute)
value = _attr if _attr is not None else self.expose_default
else: else:
value = ( value = state.state
state.state
if self.expose_attribute is None
else state.attributes.get(self.expose_attribute, self.expose_default)
)
if self.expose_type == "binary": if self.expose_type == "binary":
if value in (1, STATE_ON, "True"): if value in (1, STATE_ON, "True"):
return True return True

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"] "requirements": ["bluetooth-data-tools==1.14.0", "ld2410-ble==0.1.1"]
} }

View File

@ -32,5 +32,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble", "documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"] "requirements": ["bluetooth-data-tools==1.14.0", "led-ble==1.0.1"]
} }

View File

@ -28,9 +28,10 @@ def setup_platform(
data = hass.data[LUPUSEC_DOMAIN] data = hass.data[LUPUSEC_DOMAIN]
devices = [] device_types = [CONST.TYPE_SWITCH]
for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH): devices = []
for device in data.lupusec.get_devices(generic_type=device_types):
devices.append(LupusecSwitch(data, device)) devices.append(LupusecSwitch(data, device))
add_entities(devices) add_entities(devices)

View File

@ -7,5 +7,5 @@
"iot_class": "calculated", "iot_class": "calculated",
"loggers": ["yt_dlp"], "loggers": ["yt_dlp"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["yt-dlp==2023.9.24"] "requirements": ["yt-dlp==2023.10.13"]
} }

View File

@ -168,6 +168,7 @@ class SlaveSensor(
self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_unique_id = f"{self._attr_unique_id}_{idx}"
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_state_class = entry.get(CONF_STATE_CLASS)
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
self._attr_available = False self._attr_available = False
super().__init__(coordinator) super().__init__(coordinator)

View File

@ -63,8 +63,8 @@ PARM_IS_LEGAL = namedtuple(
], ],
) )
# PARM_IS_LEGAL defines if the keywords: # PARM_IS_LEGAL defines if the keywords:
# count: .. # count:
# structure: .. # structure:
# swap: byte # swap: byte
# swap: word # swap: word
# swap: word_byte (identical to swap: word) # swap: word_byte (identical to swap: word)
@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)), DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, True, False)),
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)), DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
} }

View File

@ -42,7 +42,6 @@ from .mixins import (
MqttAvailability, MqttAvailability,
MqttEntity, MqttEntity,
async_setup_entity_entry_helper, async_setup_entity_entry_helper,
validate_sensor_entity_category,
write_state_on_attr_change, write_state_on_attr_change,
) )
from .models import MqttValueTemplate, ReceiveMessage from .models import MqttValueTemplate, ReceiveMessage
@ -56,7 +55,7 @@ DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_FORCE_UPDATE = False DEFAULT_FORCE_UPDATE = False
CONF_EXPIRE_AFTER = "expire_after" CONF_EXPIRE_AFTER = "expire_after"
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
{ {
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
@ -68,12 +67,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
} }
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
DISCOVERY_SCHEMA = vol.All( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
)
PLATFORM_SCHEMA_MODERN = vol.All(validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE)
async def async_setup_entry( async def async_setup_entry(

View File

@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyatmo"], "loggers": ["pyatmo"],
"requirements": ["pyatmo==7.5.0"] "requirements": ["pyatmo==7.6.0"]
} }

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from aiohttp import BasicAuth from aiohttp import BasicAuth
from python_opensky import OpenSky from python_opensky import OpenSky
from python_opensky.exceptions import OpenSkyUnauthenticatedError from python_opensky.exceptions import OpenSkyError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
), ),
contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False), contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False),
) )
except OpenSkyUnauthenticatedError as exc: except OpenSkyError as exc:
raise ConfigEntryNotReady from exc raise ConfigEntryNotReady from exc
coordinator = OpenSkyDataUpdateCoordinator(hass, client) coordinator = OpenSkyDataUpdateCoordinator(hass, client)

View File

@ -6,5 +6,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/private_ble_device", "documentation": "https://www.home-assistant.io/integrations/private_ble_device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.13.0"] "requirements": ["bluetooth-data-tools==1.14.0"]
} }

View File

@ -113,7 +113,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
raise AbortFlow("already_configured") raise AbortFlow("already_configured")
# check if the camera is reachable at the new IP # check if the camera is reachable at the new IP
host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options) new_config = dict(existing_entry.data)
new_config[CONF_HOST] = discovery_info.ip
host = ReolinkHost(self.hass, new_config, existing_entry.options)
try: try:
await host.api.get_state("GetLocalLink") await host.api.get_state("GetLocalLink")
await host.api.logout() await host.api.logout()

View File

@ -211,7 +211,9 @@ async def _async_create_bridge_with_updated_data(
partial(getmac.get_mac_address, ip=host) partial(getmac.get_mac_address, ip=host)
) )
if mac: if mac and mac != "none":
# Samsung sometimes returns a value of "none" for the mac address
# this should be ignored
LOGGER.info("Updated mac to %s for %s", mac, host) LOGGER.info("Updated mac to %s for %s", mac, host)
updated_data[CONF_MAC] = mac updated_data[CONF_MAC] = mac
else: else:

View File

@ -219,7 +219,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._title = f"{self._name} ({self._model})" self._title = f"{self._name} ({self._model})"
self._udn = _strip_uuid(dev_info.get("udn", info["id"])) self._udn = _strip_uuid(dev_info.get("udn", info["id"]))
if mac := mac_from_device_info(info): if mac := mac_from_device_info(info):
self._mac = mac # Samsung sometimes returns a value of "none" for the mac address
# this should be ignored - but also shouldn't trigger getmac
if mac != "none":
self._mac = mac
elif mac := await self.hass.async_add_executor_job( elif mac := await self.hass.async_add_executor_job(
partial(getmac.get_mac_address, ip=self._host) partial(getmac.get_mac_address, ip=self._host)
): ):

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["smarttub"], "loggers": ["smarttub"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["python-smarttub==0.0.33"] "requirements": ["python-smarttub==0.0.35"]
} }

View File

@ -89,10 +89,14 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity):
"""Generic class for SmartTub status sensors.""" """Generic class for SmartTub status sensors."""
@property @property
def native_value(self) -> str: def native_value(self) -> str | None:
"""Return the current state of the sensor.""" """Return the current state of the sensor."""
if self._state is None:
return None
if isinstance(self._state, Enum): if isinstance(self._state, Enum):
return self._state.name.lower() return self._state.name.lower()
return self._state.lower() return self._state.lower()

View File

@ -32,5 +32,5 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: TailscaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: TailscaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
# Round-trip via JSON to trigger serialization # Round-trip via JSON to trigger serialization
devices = [json.loads(device.json()) for device in coordinator.data.values()] devices = [json.loads(device.to_json()) for device in coordinator.data.values()]
return async_redact_data({"devices": devices}, TO_REDACT) return async_redact_data({"devices": devices}, TO_REDACT)

View File

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["tailscale==0.2.0"] "requirements": ["tailscale==0.6.0"]
} }

View File

@ -87,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up TPLink from a config entry.""" """Set up TPLink from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
try: try:
device: SmartDevice = await Discover.discover_single(host) device: SmartDevice = await Discover.discover_single(host, timeout=10)
except SmartDeviceException as ex: except SmartDeviceException as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex

View File

@ -240,7 +240,7 @@ class TractiveClient:
self._config_entry.data[CONF_EMAIL], self._config_entry.data[CONF_EMAIL],
) )
return return
except KeyError as error: except (KeyError, TypeError) as error:
_LOGGER.error("Error while listening for events: %s", error) _LOGGER.error("Error while listening for events: %s", error)
continue continue
except aiotractive.exceptions.TractiveError: except aiotractive.exceptions.TractiveError:
@ -284,11 +284,16 @@ class TractiveClient:
) )
def _send_wellness_update(self, event: dict[str, Any]) -> None: def _send_wellness_update(self, event: dict[str, Any]) -> None:
sleep_day = None
sleep_night = None
if isinstance(event["sleep"], dict):
sleep_day = event["sleep"]["minutes_day_sleep"]
sleep_night = event["sleep"]["minutes_night_sleep"]
payload = { payload = {
ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"),
ATTR_CALORIES: event["activity"]["calories"], ATTR_CALORIES: event["activity"]["calories"],
ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], ATTR_MINUTES_DAY_SLEEP: sleep_day,
ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], ATTR_MINUTES_NIGHT_SLEEP: sleep_night,
ATTR_MINUTES_REST: event["activity"]["minutes_rest"], ATTR_MINUTES_REST: event["activity"]["minutes_rest"],
ATTR_SLEEP_LABEL: event["wellness"].get("sleep_label"), ATTR_SLEEP_LABEL: event["wellness"].get("sleep_label"),
} }

View File

@ -71,7 +71,8 @@ class WeatherFlowSensorEntityDescription(
def get_native_value(self, device: WeatherFlowDevice) -> datetime | StateType: def get_native_value(self, device: WeatherFlowDevice) -> datetime | StateType:
"""Return the parsed sensor value.""" """Return the parsed sensor value."""
raw_sensor_data = getattr(device, self.key) if (raw_sensor_data := getattr(device, self.key)) is None:
return None
return self.raw_data_conv_fn(raw_sensor_data) return self.raw_data_conv_fn(raw_sensor_data)
@ -371,14 +372,17 @@ class WeatherFlowSensorEntity(SensorEntity):
return self.device.last_report return self.device.last_report
return None return None
@property def _async_update_state(self) -> None:
def native_value(self) -> datetime | StateType: """Update entity state."""
"""Return the state of the sensor.""" value = self.entity_description.get_native_value(self.device)
return self.entity_description.get_native_value(self.device) self._attr_available = value is not None
self._attr_native_value = value
self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to events.""" """Subscribe to events."""
self._async_update_state()
for event in self.entity_description.event_subscriptions: for event in self.entity_description.event_subscriptions:
self.async_on_remove( self.async_on_remove(
self.device.on(event, lambda _: self.async_write_ha_state()) self.device.on(event, lambda _: self._async_update_state())
) )

View File

@ -37,11 +37,15 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]):
_default_update_interval: timedelta | None = UPDATE_INTERVAL _default_update_interval: timedelta | None = UPDATE_INTERVAL
_last_valid_update: datetime | None = None _last_valid_update: datetime | None = None
webhooks_connected: bool = False webhooks_connected: bool = False
coordinator_name: str = ""
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
"""Initialize the Withings data coordinator.""" """Initialize the Withings data coordinator."""
super().__init__( super().__init__(
hass, LOGGER, name="Withings", update_interval=self._default_update_interval hass,
LOGGER,
name=f"Withings {self.coordinator_name}",
update_interval=self._default_update_interval,
) )
self._client = client self._client = client
self.notification_categories: set[NotificationCategory] = set() self.notification_categories: set[NotificationCategory] = set()
@ -77,6 +81,8 @@ class WithingsMeasurementDataUpdateCoordinator(
): ):
"""Withings measurement coordinator.""" """Withings measurement coordinator."""
coordinator_name: str = "measurements"
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
"""Initialize the Withings data coordinator.""" """Initialize the Withings data coordinator."""
super().__init__(hass, client) super().__init__(hass, client)
@ -109,6 +115,8 @@ class WithingsSleepDataUpdateCoordinator(
): ):
"""Withings sleep coordinator.""" """Withings sleep coordinator."""
coordinator_name: str = "sleep"
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
"""Initialize the Withings data coordinator.""" """Initialize the Withings data coordinator."""
super().__init__(hass, client) super().__init__(hass, client)
@ -147,12 +155,16 @@ class WithingsSleepDataUpdateCoordinator(
) )
if not response: if not response:
return None return None
return response[0]
return sorted(
response, key=lambda sleep_summary: sleep_summary.end_date, reverse=True
)[0]
class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]): class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]):
"""Withings bed presence coordinator.""" """Withings bed presence coordinator."""
coordinator_name: str = "bed presence"
in_bed: bool | None = None in_bed: bool | None = None
_default_update_interval = None _default_update_interval = None
@ -178,6 +190,7 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non
class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]): class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]):
"""Withings goals coordinator.""" """Withings goals coordinator."""
coordinator_name: str = "goals"
_default_update_interval = timedelta(hours=1) _default_update_interval = timedelta(hours=1)
def webhook_subscription_listener(self, connected: bool) -> None: def webhook_subscription_listener(self, connected: bool) -> None:
@ -194,6 +207,7 @@ class WithingsActivityDataUpdateCoordinator(
): ):
"""Withings activity coordinator.""" """Withings activity coordinator."""
coordinator_name: str = "activity"
_previous_data: Activity | None = None _previous_data: Activity | None = None
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:
@ -232,6 +246,7 @@ class WithingsWorkoutDataUpdateCoordinator(
): ):
"""Withings workout coordinator.""" """Withings workout coordinator."""
coordinator_name: str = "workout"
_previous_data: Workout | None = None _previous_data: Workout | None = None
def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None:

View File

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

View File

@ -12,7 +12,7 @@ bleak-retry-connector==3.3.0
bleak==0.21.1 bleak==0.21.1
bluetooth-adapters==0.16.1 bluetooth-adapters==0.16.1
bluetooth-auto-recovery==1.2.3 bluetooth-auto-recovery==1.2.3
bluetooth-data-tools==1.13.0 bluetooth-data-tools==1.14.0
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.3.0 ciso8601==2.3.0
cryptography==41.0.4 cryptography==41.0.4
@ -22,7 +22,7 @@ ha-av==10.1.1
hass-nabucasa==0.74.0 hass-nabucasa==0.74.0
hassil==1.2.5 hassil==1.2.5
home-assistant-bluetooth==1.10.4 home-assistant-bluetooth==1.10.4
home-assistant-frontend==20231030.1 home-assistant-frontend==20231030.2
home-assistant-intents==2023.10.16 home-assistant-intents==2023.10.16
httpx==0.25.0 httpx==0.25.0
ifaddr==0.2.0 ifaddr==0.2.0

View File

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

View File

@ -192,7 +192,7 @@ aio-georss-gdacs==0.8
aioairq==0.2.4 aioairq==0.2.4
# homeassistant.components.airzone_cloud # homeassistant.components.airzone_cloud
aioairzone-cloud==0.3.1 aioairzone-cloud==0.3.5
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.9 aioairzone==0.6.9
@ -237,7 +237,7 @@ aioecowitt==2023.5.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==18.1.0 aioesphomeapi==18.2.4
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -539,7 +539,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0 blebox-uniapi==2.2.0
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.22.2 blinkpy==0.22.3
# homeassistant.components.bitcoin # homeassistant.components.bitcoin
blockchain==1.4.4 blockchain==1.4.4
@ -562,7 +562,7 @@ bluetooth-auto-recovery==1.2.3
# homeassistant.components.ld2410_ble # homeassistant.components.ld2410_ble
# homeassistant.components.led_ble # homeassistant.components.led_ble
# homeassistant.components.private_ble_device # homeassistant.components.private_ble_device
bluetooth-data-tools==1.13.0 bluetooth-data-tools==1.14.0
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.2.1 bond-async==0.2.1
@ -701,7 +701,7 @@ dovado==0.4.1
dremel3dpy==2.1.1 dremel3dpy==2.1.1
# homeassistant.components.dsmr # homeassistant.components.dsmr
dsmr-parser==1.3.0 dsmr-parser==1.3.1
# homeassistant.components.dwd_weather_warnings # homeassistant.components.dwd_weather_warnings
dwdwfsapi==1.0.6 dwdwfsapi==1.0.6
@ -1007,7 +1007,7 @@ hole==0.8.0
holidays==0.35 holidays==0.35
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20231030.1 home-assistant-frontend==20231030.2
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.10.16 home-assistant-intents==2023.10.16
@ -1094,6 +1094,9 @@ janus==1.0.0
# homeassistant.components.abode # homeassistant.components.abode
jaraco.abode==3.3.0 jaraco.abode==3.3.0
# homeassistant.components.abode
jaraco.functools==3.9.0
# homeassistant.components.jellyfin # homeassistant.components.jellyfin
jellyfin-apiclient-python==1.9.2 jellyfin-apiclient-python==1.9.2
@ -1597,7 +1600,7 @@ pyairvisual==2023.08.1
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.5.0 pyatmo==7.6.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.14.3 pyatv==0.14.3
@ -1693,7 +1696,7 @@ pyedimax==0.2.1
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.14.1 pyenphase==1.14.2
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.6 pyenvisalink==4.6
@ -2184,7 +2187,7 @@ python-ripple-api==0.0.3
python-roborock==0.35.0 python-roborock==0.35.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.35
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.15.2 python-songpal==0.15.2
@ -2533,7 +2536,7 @@ synology-srm==0.2.0
systembridgeconnector==3.8.4 systembridgeconnector==3.8.4
# homeassistant.components.tailscale # homeassistant.components.tailscale
tailscale==0.2.0 tailscale==0.6.0
# homeassistant.components.tank_utility # homeassistant.components.tank_utility
tank-utility==1.5.0 tank-utility==1.5.0
@ -2779,7 +2782,7 @@ youless-api==1.0.1
youtubeaio==1.1.5 youtubeaio==1.1.5
# homeassistant.components.media_extractor # homeassistant.components.media_extractor
yt-dlp==2023.9.24 yt-dlp==2023.10.13
# homeassistant.components.zamg # homeassistant.components.zamg
zamg==0.3.0 zamg==0.3.0

View File

@ -173,7 +173,7 @@ aio-georss-gdacs==0.8
aioairq==0.2.4 aioairq==0.2.4
# homeassistant.components.airzone_cloud # homeassistant.components.airzone_cloud
aioairzone-cloud==0.3.1 aioairzone-cloud==0.3.5
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.9 aioairzone==0.6.9
@ -218,7 +218,7 @@ aioecowitt==2023.5.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==18.1.0 aioesphomeapi==18.2.4
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -460,7 +460,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0 blebox-uniapi==2.2.0
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.22.2 blinkpy==0.22.3
# homeassistant.components.bluemaestro # homeassistant.components.bluemaestro
bluemaestro-ble==0.2.3 bluemaestro-ble==0.2.3
@ -476,7 +476,7 @@ bluetooth-auto-recovery==1.2.3
# homeassistant.components.ld2410_ble # homeassistant.components.ld2410_ble
# homeassistant.components.led_ble # homeassistant.components.led_ble
# homeassistant.components.private_ble_device # homeassistant.components.private_ble_device
bluetooth-data-tools==1.13.0 bluetooth-data-tools==1.14.0
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.2.1 bond-async==0.2.1
@ -572,7 +572,7 @@ discovery30303==0.2.1
dremel3dpy==2.1.1 dremel3dpy==2.1.1
# homeassistant.components.dsmr # homeassistant.components.dsmr
dsmr-parser==1.3.0 dsmr-parser==1.3.1
# homeassistant.components.dwd_weather_warnings # homeassistant.components.dwd_weather_warnings
dwdwfsapi==1.0.6 dwdwfsapi==1.0.6
@ -796,7 +796,7 @@ hole==0.8.0
holidays==0.35 holidays==0.35
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20231030.1 home-assistant-frontend==20231030.2
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.10.16 home-assistant-intents==2023.10.16
@ -862,6 +862,9 @@ janus==1.0.0
# homeassistant.components.abode # homeassistant.components.abode
jaraco.abode==3.3.0 jaraco.abode==3.3.0
# homeassistant.components.abode
jaraco.functools==3.9.0
# homeassistant.components.jellyfin # homeassistant.components.jellyfin
jellyfin-apiclient-python==1.9.2 jellyfin-apiclient-python==1.9.2
@ -1215,7 +1218,7 @@ pyairvisual==2023.08.1
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.5.0 pyatmo==7.6.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.14.3 pyatv==0.14.3
@ -1275,7 +1278,7 @@ pyeconet==0.1.22
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.14.1 pyenphase==1.14.2
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0
@ -1628,7 +1631,7 @@ python-qbittorrent==0.4.3
python-roborock==0.35.0 python-roborock==0.35.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.35
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.15.2 python-songpal==0.15.2
@ -1887,7 +1890,7 @@ switchbot-api==1.2.1
systembridgeconnector==3.8.4 systembridgeconnector==3.8.4
# homeassistant.components.tailscale # homeassistant.components.tailscale
tailscale==0.2.0 tailscale==0.6.0
# homeassistant.components.tellduslive # homeassistant.components.tellduslive
tellduslive==0.10.11 tellduslive==0.10.11
@ -2076,7 +2079,7 @@ youless-api==1.0.1
youtubeaio==1.1.5 youtubeaio==1.1.5
# homeassistant.components.media_extractor # homeassistant.components.media_extractor
yt-dlp==2023.9.24 yt-dlp==2023.10.13
# homeassistant.components.zamg # homeassistant.components.zamg
zamg==0.3.0 zamg==0.3.0

View File

@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
from dsmr_parser.clients.protocol import DSMRProtocol from dsmr_parser.clients.protocol import DSMRProtocol
from dsmr_parser.clients.rfxtrx_protocol import RFXtrxDSMRProtocol from dsmr_parser.clients.rfxtrx_protocol import RFXtrxDSMRProtocol
from dsmr_parser.obis_references import ( from dsmr_parser.obis_references import (
BELGIUM_EQUIPMENT_IDENTIFIER,
EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER,
EQUIPMENT_IDENTIFIER_GAS, EQUIPMENT_IDENTIFIER_GAS,
LUXEMBOURG_EQUIPMENT_IDENTIFIER, LUXEMBOURG_EQUIPMENT_IDENTIFIER,
@ -81,6 +82,15 @@ async def dsmr_connection_send_validate_fixture(hass):
async def connection_factory(*args, **kwargs): async def connection_factory(*args, **kwargs):
"""Return mocked out Asyncio classes.""" """Return mocked out Asyncio classes."""
if args[1] == "5B":
protocol.telegram = {
BELGIUM_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_EQUIPMENT_IDENTIFIER, [{"value": "12345678", "unit": ""}]
),
EQUIPMENT_IDENTIFIER_GAS: CosemObject(
EQUIPMENT_IDENTIFIER_GAS, [{"value": "123456789", "unit": ""}]
),
}
if args[1] == "5L": if args[1] == "5L":
protocol.telegram = { protocol.telegram = {
LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject( LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject(

View File

@ -215,6 +215,50 @@ async def test_setup_serial_rfxtrx(
assert result["data"] == {**entry_data, **SERIAL_DATA} assert result["data"] == {**entry_data, **SERIAL_DATA}
@patch("serial.tools.list_ports.comports", return_value=[com_port()])
async def test_setup_5B(
com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture
) -> None:
"""Test we can setup serial."""
port = com_port()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"type": "Serial"},
)
assert result["type"] == "form"
assert result["step_id"] == "setup_serial"
assert result["errors"] == {}
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"port": port.device, "dsmr_version": "5B"},
)
await hass.async_block_till_done()
entry_data = {
"port": port.device,
"dsmr_version": "5B",
"protocol": "dsmr_protocol",
"serial_id": "12345678",
"serial_id_gas": "123456789",
}
assert result["type"] == "create_entry"
assert result["title"] == port.device
assert result["data"] == entry_data
@patch("serial.tools.list_ports.comports", return_value=[com_port()]) @patch("serial.tools.list_ports.comports", return_value=[com_port()])
async def test_setup_5L( async def test_setup_5L(
com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture

View File

@ -8,10 +8,22 @@ import asyncio
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from itertools import chain, repeat from itertools import chain, repeat
from typing import Literal
from unittest.mock import DEFAULT, MagicMock from unittest.mock import DEFAULT, MagicMock
from dsmr_parser.obis_references import (
BELGIUM_MBUS1_METER_READING1,
BELGIUM_MBUS1_METER_READING2,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS2_METER_READING2,
BELGIUM_MBUS3_METER_READING1,
BELGIUM_MBUS3_METER_READING2,
BELGIUM_MBUS4_METER_READING1,
BELGIUM_MBUS4_METER_READING2,
)
import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.dsmr.const import BELGIUM_5MIN_GAS_METER_READING
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_OPTIONS, ATTR_OPTIONS,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
@ -483,6 +495,10 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
from dsmr_parser.obis_references import ( from dsmr_parser.obis_references import (
BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_CURRENT_AVERAGE_DEMAND,
BELGIUM_MAXIMUM_DEMAND_MONTH, BELGIUM_MAXIMUM_DEMAND_MONTH,
BELGIUM_MBUS1_METER_READING2,
BELGIUM_MBUS2_METER_READING2,
BELGIUM_MBUS3_METER_READING2,
BELGIUM_MBUS4_METER_READING2,
ELECTRICITY_ACTIVE_TARIFF, ELECTRICITY_ACTIVE_TARIFF,
) )
from dsmr_parser.objects import CosemObject, MBusObject from dsmr_parser.objects import CosemObject, MBusObject
@ -500,13 +516,34 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
} }
telegram = { telegram = {
BELGIUM_5MIN_GAS_METER_READING: MBusObject( BELGIUM_MBUS1_METER_READING2: MBusObject(
BELGIUM_5MIN_GAS_METER_READING, BELGIUM_MBUS1_METER_READING2,
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"}, {"value": Decimal(745.695), "unit": "m3"},
], ],
), ),
BELGIUM_MBUS2_METER_READING2: MBusObject(
BELGIUM_MBUS2_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642214)},
{"value": Decimal(745.696), "unit": "m3"},
],
),
BELGIUM_MBUS3_METER_READING2: MBusObject(
BELGIUM_MBUS3_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642215)},
{"value": Decimal(745.697), "unit": "m3"},
],
),
BELGIUM_MBUS4_METER_READING2: MBusObject(
BELGIUM_MBUS4_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642216)},
{"value": Decimal(745.698), "unit": "m3"},
],
),
BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject( BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject(
BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_CURRENT_AVERAGE_DEMAND,
[{"value": Decimal(1.75), "unit": "kW"}], [{"value": Decimal(1.75), "unit": "kW"}],
@ -577,6 +614,115 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
) )
@pytest.mark.parametrize(
("key1", "key2", "key3", "gas_value"),
[
(
BELGIUM_MBUS1_METER_READING1,
BELGIUM_MBUS2_METER_READING2,
BELGIUM_MBUS3_METER_READING1,
"745.696",
),
(
BELGIUM_MBUS1_METER_READING2,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS3_METER_READING2,
"745.695",
),
(
BELGIUM_MBUS4_METER_READING2,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS3_METER_READING1,
"745.695",
),
(
BELGIUM_MBUS4_METER_READING1,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS3_METER_READING2,
"745.697",
),
],
)
async def test_belgian_meter_alt(
hass: HomeAssistant,
dsmr_connection_fixture,
key1: Literal,
key2: Literal,
key3: Literal,
gas_value: str,
) -> None:
"""Test if Belgian meter is correctly parsed."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.objects import MBusObject
entry_data = {
"port": "/dev/ttyUSB0",
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
entry_options = {
"time_between_update": 0,
}
telegram = {
key1: MBusObject(
key1,
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
),
key2: MBusObject(
key2,
[
{"value": datetime.datetime.fromtimestamp(1551642214)},
{"value": Decimal(745.696), "unit": "m3"},
],
),
key3: MBusObject(
key3,
[
{"value": datetime.datetime.fromtimestamp(1551642215)},
{"value": Decimal(745.697), "unit": "m3"},
],
),
}
mock_entry = MockConfigEntry(
domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
telegram_callback = connection_factory.call_args_list[0][0][2]
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
# after receiving telegram entities need to have the chance to be created
await hass.async_block_till_done()
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
assert gas_consumption.state == gas_value
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS
assert (
gas_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None:
"""Test if Belgian meter is correctly parsed.""" """Test if Belgian meter is correctly parsed."""
(connection_factory, transport, protocol) = dsmr_connection_fixture (connection_factory, transport, protocol) = dsmr_connection_fixture

View File

@ -77,6 +77,17 @@ def mock_config_entry(hass) -> MockConfigEntry:
return config_entry return config_entry
class BaseMockReconnectLogic(ReconnectLogic):
"""Mock ReconnectLogic."""
def stop_callback(self) -> None:
"""Stop the reconnect logic."""
# For the purposes of testing, we don't want to wait
# for the reconnect logic to finish trying to connect
self._cancel_connect("forced disconnect from test")
self._is_stopped = True
@pytest.fixture @pytest.fixture
def mock_device_info() -> DeviceInfo: def mock_device_info() -> DeviceInfo:
"""Return the default mocked device info.""" """Return the default mocked device info."""
@ -132,7 +143,10 @@ def mock_client(mock_device_info) -> APIClient:
mock_client.address = "127.0.0.1" mock_client.address = "127.0.0.1"
mock_client.api_version = APIVersion(99, 99) mock_client.api_version = APIVersion(99, 99)
with patch("homeassistant.components.esphome.APIClient", mock_client), patch( with patch(
"homeassistant.components.esphome.manager.ReconnectLogic",
BaseMockReconnectLogic,
), patch("homeassistant.components.esphome.APIClient", mock_client), patch(
"homeassistant.components.esphome.config_flow.APIClient", mock_client "homeassistant.components.esphome.config_flow.APIClient", mock_client
): ):
yield mock_client yield mock_client
@ -234,7 +248,7 @@ async def _mock_generic_device_entry(
try_connect_done = Event() try_connect_done = Event()
class MockReconnectLogic(ReconnectLogic): class MockReconnectLogic(BaseMockReconnectLogic):
"""Mock ReconnectLogic.""" """Mock ReconnectLogic."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -250,6 +264,13 @@ async def _mock_generic_device_entry(
try_connect_done.set() try_connect_done.set()
return result return result
def stop_callback(self) -> None:
"""Stop the reconnect logic."""
# For the purposes of testing, we don't want to wait
# for the reconnect logic to finish trying to connect
self._cancel_connect("forced disconnect from test")
self._is_stopped = True
with patch( with patch(
"homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic "homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic
): ):

View File

@ -85,6 +85,14 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None:
hass.states.async_set(entity_id, "off", {}) hass.states.async_set(entity_id, "off", {})
await knx.assert_telegram_count(0) await knx.assert_telegram_count(0)
# Change attribute; keep state
hass.states.async_set(entity_id, "on", {attribute: 1})
await knx.assert_write("1/1/8", (1,))
# Change state to "off"; null attribute
hass.states.async_set(entity_id, "off", {attribute: None})
await knx.assert_telegram_count(0)
async def test_expose_attribute_with_default( async def test_expose_attribute_with_default(
hass: HomeAssistant, knx: KNXTestKit hass: HomeAssistant, knx: KNXTestKit
@ -132,6 +140,14 @@ async def test_expose_attribute_with_default(
hass.states.async_set(entity_id, "off", {}) hass.states.async_set(entity_id, "off", {})
await knx.assert_write("1/1/8", (0,)) await knx.assert_write("1/1/8", (0,))
# Change state and attribute
hass.states.async_set(entity_id, "on", {attribute: 1})
await knx.assert_write("1/1/8", (1,))
# Change state to "off"; null attribute
hass.states.async_set(entity_id, "off", {attribute: None})
await knx.assert_write("1/1/8", (0,))
async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None:
"""Test an expose to send string values of up to 14 bytes only.""" """Test an expose to send string values of up to 14 bytes only."""

View File

@ -1,10 +1,11 @@
"""Test the Litter-Robot time entity.""" """Test the Litter-Robot time entity."""
from __future__ import annotations from __future__ import annotations
from datetime import time from datetime import datetime, time
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pylitterbot import LitterRobot3 from pylitterbot import LitterRobot3
import pytest
from homeassistant.components.time import DOMAIN as PLATFORM_DOMAIN from homeassistant.components.time import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
@ -15,6 +16,7 @@ from .conftest import setup_integration
SLEEP_START_TIME_ENTITY_ID = "time.test_sleep_mode_start_time" SLEEP_START_TIME_ENTITY_ID = "time.test_sleep_mode_start_time"
@pytest.mark.freeze_time(datetime(2023, 7, 1, 12))
async def test_sleep_mode_start_time( async def test_sleep_mode_start_time(
hass: HomeAssistant, mock_account: MagicMock hass: HomeAssistant, mock_account: MagicMock
) -> None: ) -> None:

View File

@ -513,6 +513,20 @@ async def test_config_wrong_struct_sensor(
False, False,
"07-05-2020 14:35", "07-05-2020 14:35",
), ),
(
{
CONF_COUNT: 8,
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DataType.STRING,
CONF_SWAP: CONF_SWAP_BYTE,
CONF_SCALE: 1,
CONF_OFFSET: 0,
CONF_PRECISION: 0,
},
[0x3730, 0x302D, 0x2D35, 0x3032, 0x3032, 0x3120, 0x3A34, 0x3533],
False,
"07-05-2020 14:35",
),
( (
{ {
CONF_COUNT: 8, CONF_COUNT: 8,

View File

@ -2163,15 +2163,6 @@ async def test_setup_manual_mqtt_with_invalid_config(
} }
} }
}, },
{
mqtt.DOMAIN: {
"binary_sensor": {
"name": "test",
"state_topic": "test-topic",
"entity_category": "config",
}
}
},
], ],
) )
@patch( @patch(

View File

@ -572,6 +572,7 @@
'read_smokedetector', 'read_smokedetector',
'read_station', 'read_station',
'read_thermostat', 'read_thermostat',
'read_mhs1',
'write_bubendorff', 'write_bubendorff',
'write_camera', 'write_camera',
'write_magellan', 'write_magellan',
@ -579,6 +580,7 @@
'write_presence', 'write_presence',
'write_smarther', 'write_smarther',
'write_thermostat', 'write_thermostat',
'write_mhs1',
]), ]),
'type': 'Bearer', 'type': 'Bearer',
}), }),

View File

@ -34,8 +34,10 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
@pytest.fixture @pytest.fixture
def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None]: def reolink_connect_class(
"""Mock reolink connection.""" mock_get_source_ip: None,
) -> Generator[MagicMock, None, None]:
"""Mock reolink connection and return both the host_mock and host_mock_class."""
with patch( with patch(
"homeassistant.components.reolink.host.webhook.async_register", "homeassistant.components.reolink.host.webhook.async_register",
return_value=True, return_value=True,
@ -65,7 +67,15 @@ def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None
host_mock.session_active = True host_mock.session_active = True
host_mock.timeout = 60 host_mock.timeout = 60
host_mock.renewtimer.return_value = 600 host_mock.renewtimer.return_value = 600
yield host_mock yield host_mock_class
@pytest.fixture
def reolink_connect(
reolink_connect_class: MagicMock,
) -> Generator[MagicMock, None, None]:
"""Mock reolink connection."""
return reolink_connect_class.return_value
@pytest.fixture @pytest.fixture

View File

@ -2,7 +2,7 @@
from datetime import timedelta from datetime import timedelta
import json import json
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock, call
import pytest import pytest
from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError
@ -12,6 +12,7 @@ from homeassistant.components import dhcp
from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
from homeassistant.components.reolink.exceptions import ReolinkWebhookException from homeassistant.components.reolink.exceptions import ReolinkWebhookException
from homeassistant.components.reolink.host import DEFAULT_TIMEOUT
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -380,41 +381,47 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No
@pytest.mark.parametrize( @pytest.mark.parametrize(
("last_update_success", "attr", "value", "expected"), ("last_update_success", "attr", "value", "expected", "host_call_list"),
[ [
( (
False, False,
None, None,
None, None,
TEST_HOST2, TEST_HOST2,
[TEST_HOST, TEST_HOST2],
), ),
( (
True, True,
None, None,
None, None,
TEST_HOST, TEST_HOST,
[TEST_HOST],
), ),
( (
False, False,
"get_state", "get_state",
AsyncMock(side_effect=ReolinkError("Test error")), AsyncMock(side_effect=ReolinkError("Test error")),
TEST_HOST, TEST_HOST,
[TEST_HOST, TEST_HOST2],
), ),
( (
False, False,
"mac_address", "mac_address",
"aa:aa:aa:aa:aa:aa", "aa:aa:aa:aa:aa:aa",
TEST_HOST, TEST_HOST,
[TEST_HOST, TEST_HOST2],
), ),
], ],
) )
async def test_dhcp_ip_update( async def test_dhcp_ip_update(
hass: HomeAssistant, hass: HomeAssistant,
reolink_connect_class: MagicMock,
reolink_connect: MagicMock, reolink_connect: MagicMock,
last_update_success: bool, last_update_success: bool,
attr: str, attr: str,
value: Any, value: Any,
expected: str, expected: str,
host_call_list: list[str],
) -> None: ) -> None:
"""Test dhcp discovery aborts if already configured where the IP is updated if appropriate.""" """Test dhcp discovery aborts if already configured where the IP is updated if appropriate."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -459,6 +466,22 @@ async def test_dhcp_ip_update(
const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data
) )
expected_calls = []
for host in host_call_list:
expected_calls.append(
call(
host,
TEST_USERNAME,
TEST_PASSWORD,
port=TEST_PORT,
use_https=TEST_USE_HTTPS,
protocol=DEFAULT_PROTOCOL,
timeout=DEFAULT_TIMEOUT,
)
)
assert reolink_connect_class.call_args_list == expected_calls
assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["type"] is data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"

View File

@ -1,4 +1,5 @@
"""Tests for Samsung TV config flow.""" """Tests for Samsung TV config flow."""
from copy import deepcopy
from ipaddress import ip_address from ipaddress import ip_address
from unittest.mock import ANY, AsyncMock, Mock, call, patch from unittest.mock import ANY, AsyncMock, Mock, call, patch
@ -165,14 +166,6 @@ MOCK_DEVICE_INFO = {
}, },
"id": "123", "id": "123",
} }
MOCK_DEVICE_INFO_2 = {
"device": {
"type": "Samsung SmartTV",
"name": "fake2_name",
"modelName": "fake2_model",
},
"id": "345",
}
AUTODETECT_LEGACY = { AUTODETECT_LEGACY = {
"name": "HomeAssistant", "name": "HomeAssistant",
@ -1968,3 +1961,56 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp(
assert result["step_id"] == "confirm" assert result["step_id"] == "confirm"
assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp"
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
@pytest.mark.usefixtures("remotews", "remoteencws_failing")
async def test_ssdp_update_mac(hass: HomeAssistant) -> None:
"""Ensure that MAC address is correctly updated from SSDP."""
with patch(
"homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info",
return_value=MOCK_DEVICE_INFO,
):
# entry was added
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
)
assert result["type"] == FlowResultType.CREATE_ENTRY
entry = result["result"]
assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER
assert entry.data[CONF_MODEL] == "fake_model"
assert entry.data[CONF_MAC] is None
assert entry.unique_id == "123"
device_info = deepcopy(MOCK_DEVICE_INFO)
device_info["device"]["wifiMac"] = "none"
with patch(
"homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info",
return_value=device_info,
):
# Updated
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == RESULT_ALREADY_CONFIGURED
# ensure mac wasn't updated with "none"
assert entry.data[CONF_MAC] is None
assert entry.unique_id == "123"
device_info = deepcopy(MOCK_DEVICE_INFO)
device_info["device"]["wifiMac"] = "aa:bb:cc:dd:ee:ff"
with patch(
"homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info",
return_value=device_info,
):
# Updated
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == RESULT_ALREADY_CONFIGURED
# ensure mac was updated with new wifiMac value
assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
assert entry.unique_id == "123"

View File

@ -2,6 +2,7 @@
import pytest import pytest
import smarttub import smarttub
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -27,6 +28,27 @@ async def test_sensor(
assert state.state == expected_state assert state.state == expected_state
# https://github.com/home-assistant/core/issues/102339
async def test_null_blowoutcycle(
spa,
spa_state,
config_entry,
hass: HomeAssistant,
) -> None:
"""Test blowoutCycle having null value."""
spa_state.blowout_cycle = None
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = f"sensor.{spa.brand}_{spa.model}_blowout_cycle"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
async def test_primary_filtration( async def test_primary_filtration(
spa, spa_state, setup_entry, hass: HomeAssistant spa, spa_state, setup_entry, hass: HomeAssistant
) -> None: ) -> None:

View File

@ -41,7 +41,7 @@ def mock_tailscale_config_flow() -> Generator[None, MagicMock, None]:
"homeassistant.components.tailscale.config_flow.Tailscale", autospec=True "homeassistant.components.tailscale.config_flow.Tailscale", autospec=True
) as tailscale_mock: ) as tailscale_mock:
tailscale = tailscale_mock.return_value tailscale = tailscale_mock.return_value
tailscale.devices.return_value = Devices.parse_raw( tailscale.devices.return_value = Devices.from_json(
load_fixture("tailscale/devices.json") load_fixture("tailscale/devices.json")
).devices ).devices
yield tailscale yield tailscale
@ -54,7 +54,7 @@ def mock_tailscale(request: pytest.FixtureRequest) -> Generator[None, MagicMock,
if hasattr(request, "param") and request.param: if hasattr(request, "param") and request.param:
fixture = request.param fixture = request.param
devices = Devices.parse_raw(load_fixture(fixture)).devices devices = Devices.from_json(load_fixture(fixture)).devices
with patch( with patch(
"homeassistant.components.tailscale.coordinator.Tailscale", autospec=True "homeassistant.components.tailscale.coordinator.Tailscale", autospec=True
) as tailscale_mock: ) as tailscale_mock:

View File

@ -0,0 +1,87 @@
# serializer version: 1
# name: test_diagnostics
dict({
'devices': list([
dict({
'addresses': '**REDACTED**',
'advertised_routes': list([
]),
'authorized': True,
'blocks_incoming_connections': False,
'client_connectivity': dict({
'client_supports': dict({
'hair_pinning': False,
'ipv6': False,
'pcp': False,
'pmp': False,
'udp': True,
'upnp': False,
}),
'endpoints': '**REDACTED**',
'mapping_varies_by_dest_ip': False,
}),
'client_version': '1.12.3-td91ea7286-ge1bbbd90c',
'created': '2021-08-19T09:25:22+00:00',
'device_id': '**REDACTED**',
'enabled_routes': list([
]),
'expires': '2022-02-15T09:25:22+00:00',
'hostname': '**REDACTED**',
'is_external': False,
'key_expiry_disabled': False,
'last_seen': '2021-09-16T06:11:23+00:00',
'machine_key': '**REDACTED**',
'name': '**REDACTED**',
'node_key': '**REDACTED**',
'os': 'iOS',
'tags': list([
]),
'update_available': True,
'user': '**REDACTED**',
}),
dict({
'addresses': '**REDACTED**',
'advertised_routes': list([
'0.0.0.0/0',
'10.10.10.0/23',
'::/0',
]),
'authorized': True,
'blocks_incoming_connections': False,
'client_connectivity': dict({
'client_supports': dict({
'hair_pinning': True,
'ipv6': False,
'pcp': False,
'pmp': False,
'udp': True,
'upnp': False,
}),
'endpoints': '**REDACTED**',
'mapping_varies_by_dest_ip': False,
}),
'client_version': '1.14.0-t5cff36945-g809e87bba',
'created': '2021-08-29T09:49:06+00:00',
'device_id': '**REDACTED**',
'enabled_routes': list([
'0.0.0.0/0',
'10.10.10.0/23',
'::/0',
]),
'expires': '2022-02-25T09:49:06+00:00',
'hostname': '**REDACTED**',
'is_external': False,
'key_expiry_disabled': False,
'last_seen': '2021-11-15T20:37:03+00:00',
'machine_key': '**REDACTED**',
'name': '**REDACTED**',
'node_key': '**REDACTED**',
'os': 'linux',
'tags': list([
]),
'update_available': True,
'user': '**REDACTED**',
}),
]),
})
# ---

View File

@ -1,6 +1,6 @@
"""Tests for the diagnostics data provided by the Tailscale integration.""" """Tests for the diagnostics data provided by the Tailscale integration."""
from syrupy import SnapshotAssertion
from homeassistant.components.diagnostics import REDACTED
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -12,93 +12,10 @@ async def test_diagnostics(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test diagnostics.""" """Test diagnostics."""
assert await get_diagnostics_for_config_entry( assert (
hass, hass_client, init_integration await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
) == { == snapshot
"devices": [ )
{
"addresses": REDACTED,
"device_id": REDACTED,
"user": REDACTED,
"name": REDACTED,
"hostname": REDACTED,
"client_version": "1.12.3-td91ea7286-ge1bbbd90c",
"update_available": True,
"os": "iOS",
"created": "2021-08-19T09:25:22+00:00",
"last_seen": "2021-09-16T06:11:23+00:00",
"key_expiry_disabled": False,
"expires": "2022-02-15T09:25:22+00:00",
"authorized": True,
"is_external": False,
"machine_key": REDACTED,
"node_key": REDACTED,
"blocks_incoming_connections": False,
"enabled_routes": [],
"advertised_routes": [],
"client_connectivity": {
"endpoints": REDACTED,
"derp": "",
"mapping_varies_by_dest_ip": False,
"latency": {},
"client_supports": {
"hair_pinning": False,
"ipv6": False,
"pcp": False,
"pmp": False,
"udp": True,
"upnp": False,
},
},
},
{
"addresses": REDACTED,
"device_id": REDACTED,
"user": REDACTED,
"name": REDACTED,
"hostname": REDACTED,
"client_version": "1.14.0-t5cff36945-g809e87bba",
"update_available": True,
"os": "linux",
"created": "2021-08-29T09:49:06+00:00",
"last_seen": "2021-11-15T20:37:03+00:00",
"key_expiry_disabled": False,
"expires": "2022-02-25T09:49:06+00:00",
"authorized": True,
"is_external": False,
"machine_key": REDACTED,
"node_key": REDACTED,
"blocks_incoming_connections": False,
"enabled_routes": ["0.0.0.0/0", "10.10.10.0/23", "::/0"],
"advertised_routes": ["0.0.0.0/0", "10.10.10.0/23", "::/0"],
"client_connectivity": {
"endpoints": REDACTED,
"derp": "",
"mapping_varies_by_dest_ip": False,
"latency": {
"Bangalore": {"latencyMs": 143.42505599999998},
"Chicago": {"latencyMs": 101.123646},
"Dallas": {"latencyMs": 136.85886},
"Frankfurt": {"latencyMs": 18.968314},
"London": {"preferred": True, "latencyMs": 14.314574},
"New York City": {"latencyMs": 83.078912},
"San Francisco": {"latencyMs": 148.215522},
"Seattle": {"latencyMs": 181.553595},
"Singapore": {"latencyMs": 164.566539},
"São Paulo": {"latencyMs": 207.250179},
"Tokyo": {"latencyMs": 226.90714300000002},
},
"client_supports": {
"hair_pinning": True,
"ipv6": False,
"pcp": False,
"pmp": False,
"udp": True,
"upnp": False,
},
},
},
]
}

View File

@ -202,7 +202,7 @@ def _patch_discovery(device=None, no_device=False):
def _patch_single_discovery(device=None, no_device=False): def _patch_single_discovery(device=None, no_device=False):
async def _discover_single(*_): async def _discover_single(*args, **kwargs):
if no_device: if no_device:
raise SmartDeviceException raise SmartDeviceException
return device if device else _mocked_bulb() return device if device else _mocked_bulb()

View File

@ -1,43 +1,4 @@
[ [
{
"id": 2081804182,
"timezone": "Europe/Paris",
"model": 32,
"model_id": 63,
"hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee",
"startdate": 1618691453,
"enddate": 1618713173,
"date": "2021-04-18",
"data": {
"wakeupduration": 3060,
"wakeupcount": 1,
"durationtosleep": 540,
"remsleepduration": 2400,
"durationtowakeup": 1140,
"total_sleep_time": 18660,
"sleep_efficiency": 0.86,
"sleep_latency": 540,
"wakeup_latency": 1140,
"waso": 1380,
"nb_rem_episodes": 1,
"out_of_bed_count": 0,
"lightsleepduration": 10440,
"deepsleepduration": 5820,
"hr_average": 103,
"hr_min": 70,
"hr_max": 120,
"rr_average": 14,
"rr_min": 10,
"rr_max": 20,
"breathing_disturbances_intensity": 9,
"snoring": 1080,
"snoringepisodecount": 18,
"sleep_score": 37,
"apnea_hypopnea_index": 9
},
"created": 1620237476,
"modified": 1620237476
},
{ {
"id": 2081804265, "id": 2081804265,
"timezone": "Europe/Paris", "timezone": "Europe/Paris",
@ -77,6 +38,45 @@
"created": 1620237480, "created": 1620237480,
"modified": 1620237479 "modified": 1620237479
}, },
{
"id": 2081804182,
"timezone": "Europe/Paris",
"model": 32,
"model_id": 63,
"hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee",
"startdate": 1618691453,
"enddate": 1618713173,
"date": "2021-04-18",
"data": {
"wakeupduration": 3060,
"wakeupcount": 1,
"durationtosleep": 540,
"remsleepduration": 2400,
"durationtowakeup": 1140,
"total_sleep_time": 18660,
"sleep_efficiency": 0.86,
"sleep_latency": 540,
"wakeup_latency": 1140,
"waso": 1380,
"nb_rem_episodes": 1,
"out_of_bed_count": 0,
"lightsleepduration": 10440,
"deepsleepduration": 5820,
"hr_average": 103,
"hr_min": 70,
"hr_max": 120,
"rr_average": 14,
"rr_min": 10,
"rr_max": 20,
"breathing_disturbances_intensity": 9,
"snoring": 1080,
"snoringepisodecount": 18,
"sleep_score": 37,
"apnea_hypopnea_index": 9
},
"created": 1620237476,
"modified": 1620237476
},
{ {
"id": 2081804358, "id": 2081804358,
"timezone": "Europe/Paris", "timezone": "Europe/Paris",