This commit is contained in:
Franck Nijhof 2024-07-05 17:25:40 +02:00 committed by GitHub
commit 1cf62916a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 770 additions and 214 deletions

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.14.0"]
"requirements": ["anova-wifi==0.15.0"]
}

View File

@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.1.7"]
"requirements": ["aioaquacell==0.1.8"]
}

View File

@ -2,10 +2,10 @@
from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from path import Path
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"]
}

View File

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

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.51", "babel==2.15.0"]
"requirements": ["holidays==0.52", "babel==2.15.0"]
}

View File

@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity):
@property
def unique_id(self) -> str:
"""Return a unique ID."""
suffix = ""
if self._post is not None:
suffix = f"_{self._post}"
unique_id = f"{self.__class__.__name__}_{self._device.id}"
if self._is_multi_channel:
return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}"
unique_id = (
f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
)
return f"{self.__class__.__name__}_{self._device.id}{suffix}"
return unique_id
@property
def icon(self) -> str | None:

View File

@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homematicip.aio.device import (
@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@ -163,20 +161,29 @@ async def async_setup_entry(
for ch in get_channels_from_device(
device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL
):
if ch.connectedEnergySensorType not in SENSORS_ESI:
continue
new_entities = [
HmipEsiSensorEntity(hap, device, ch.index, description)
for description in SENSORS_ESI[ch.connectedEnergySensorType]
]
entities.extend(
entity
for entity in new_entities
if entity.entity_description.exists_fn(ch)
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC:
if ch.currentPowerConsumption is not None:
entities.append(HmipEsiIecPowerConsumption(hap, device))
if ch.energyCounterOneType != ESI_TYPE_UNKNOWN:
entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device))
if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN:
entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device))
if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN:
entities.append(
HmipEsiIecEnergyCounterInputSingleTariff(hap, device)
)
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS:
if ch.currentGasFlow is not None:
entities.append(HmipEsiGasCurrentGasFlow(hap, device))
if ch.gasVolume is not None:
entities.append(HmipEsiGasGasVolume(hap, device))
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED:
if ch.currentPowerConsumption is not None:
entities.append(HmipEsiLedCurrentPowerConsumption(hap, device))
entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device))
async_add_entities(entities)
@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE
return self._device.temperatureExternalDelta
@dataclass(kw_only=True, frozen=True)
class HmipEsiSensorEntityDescription(SensorEntityDescription):
"""SensorEntityDescription for HmIP Sensors."""
value_fn: Callable[[AsyncEnergySensorsInterface], StateType]
exists_fn: Callable[[FunctionalChannel], bool]
type_fn: Callable[[AsyncEnergySensorsInterface], str]
SENSORS_ESI = {
ESI_CONNECTED_SENSOR_TYPE_IEC: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
type_fn=lambda device: "CurrentPowerConsumption",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterOne,
exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterOneType,
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterTwo,
exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterTwoType,
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterThree,
exists_fn=lambda channel: channel.energyCounterThreeType
!= ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterThreeType,
),
],
ESI_CONNECTED_SENSOR_TYPE_LED: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
type_fn=lambda device: "CurrentPowerConsumption",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterOne,
exists_fn=lambda channel: channel.energyCounterOne is not None,
type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
),
],
ESI_CONNECTED_SENSOR_TYPE_GAS: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_GAS_FLOW,
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentGasFlow,
exists_fn=lambda channel: channel.currentGasFlow is not None,
type_fn=lambda device: "CurrentGasFlow",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_GAS_VOLUME,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.gasVolume,
exists_fn=lambda channel: channel.gasVolume is not None,
type_fn=lambda device: "GasVolume",
),
],
}
class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
"""EntityDescription for HmIP-ESI Sensors."""
entity_description: HmipEsiSensorEntityDescription
def __init__(
self,
hap: HomematicipHAP,
device: HomematicipGenericEntity,
channel_index: int,
entity_description: HmipEsiSensorEntityDescription,
key: str,
value_fn: Callable[[FunctionalChannel], StateType],
type_fn: Callable[[FunctionalChannel], str],
) -> None:
"""Initialize Sensor Entity."""
super().__init__(
hap=hap,
device=device,
channel=channel_index,
post=entity_description.key,
channel=1,
post=key,
is_multi_channel=False,
)
self.entity_description = entity_description
self._value_fn = value_fn
self._type_fn = type_fn
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the esi sensor."""
state_attr = super().extra_state_attributes
state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self)
state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel)
return state_attr
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return str(self.entity_description.value_fn(self))
return str(self._value_fn(self.functional_channel))
class HmipEsiIecPowerConsumption(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC currentPowerConsumption sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentPowerConsumption",
value_fn=lambda channel: channel.currentPowerConsumption,
type_fn=lambda channel: "CurrentPowerConsumption",
)
class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterOne sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
value_fn=lambda channel: channel.energyCounterOne,
type_fn=lambda channel: channel.energyCounterOneType,
)
class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterTwo sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
value_fn=lambda channel: channel.energyCounterTwo,
type_fn=lambda channel: channel.energyCounterTwoType,
)
class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterThree sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
value_fn=lambda channel: channel.energyCounterThree,
type_fn=lambda channel: channel.energyCounterThreeType,
)
class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI Gas currentGasFlow sensor."""
_attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentGasFlow",
value_fn=lambda channel: channel.currentGasFlow,
type_fn=lambda channel: "CurrentGasFlow",
)
class HmipEsiGasGasVolume(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI Gas gasVolume sensor."""
_attr_device_class = SensorDeviceClass.GAS
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="GasVolume",
value_fn=lambda channel: channel.gasVolume,
type_fn=lambda channel: "GasVolume",
)
class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI LED currentPowerConsumption sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentPowerConsumption",
value_fn=lambda channel: channel.currentPowerConsumption,
type_fn=lambda channel: "CurrentPowerConsumption",
)
class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI LED energyCounterOne sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
value_fn=lambda channel: channel.energyCounterOne,
type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
)
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):

View File

@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [
RestrictedReasons.WEEK_SCHEDULE.lower(),
]
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
@callback
def _get_work_area_names(data: MowerAttributes) -> list[str]:
@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
if TYPE_CHECKING:
# Sensor does not get created if it is None
assert data.work_areas is not None
return [data.work_areas[work_area_id].name for work_area_id in data.work_areas]
work_area_list = [
data.work_areas[work_area_id].name for work_area_id in data.work_areas
]
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
return work_area_list
@callback
def _get_current_work_area_name(data: MowerAttributes) -> str:
"""Return the name of the current work area."""
if data.mower.work_area_id is None:
return STATE_NO_WORK_AREA_ACTIVE
if TYPE_CHECKING:
# Sensor does not get created if values are None
assert data.work_areas is not None
assert data.mower.work_area_id is not None
return data.work_areas[data.mower.work_area_id].name

View File

@ -252,7 +252,8 @@
"work_area": {
"name": "Work area",
"state": {
"my_lawn": "My lawn"
"my_lawn": "My lawn",
"no_work_area_active": "No work area active"
}
}
},

View File

@ -28,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.5.7"]
"requirements": ["inkbird-ble==0.5.8"]
}

View File

@ -350,6 +350,7 @@ DISCOVERY_SCHEMAS = [
clusters.Thermostat.Attributes.TemperatureSetpointHold,
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
clusters.OnOff.Attributes.OnOff,
),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
),

View File

@ -313,6 +313,7 @@ DISCOVERY_SCHEMAS = [
clusters.FanControl.Attributes.RockSetting,
clusters.FanControl.Attributes.WindSetting,
clusters.FanControl.Attributes.AirflowDirection,
clusters.OnOff.Attributes.OnOff,
),
),
]

View File

@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [
device_types.DimmablePlugInUnit,
device_types.ExtendedColorLight,
device_types.OnOffLight,
device_types.DimmerSwitch,
device_types.ColorDimmerSwitch,
),
),
# Additional schema to match (HS Color) lights with incorrect/missing device type

View File

@ -90,6 +90,9 @@ class MatterLock(MatterEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock with pin if needed."""
# optimistically signal locking to state machine
self._attr_is_locking = True
self.async_write_ha_state()
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
@ -98,6 +101,9 @@ class MatterLock(MatterEntity, LockEntity):
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock with pin if needed."""
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
self.async_write_ha_state()
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
if self.supports_unbolt:
@ -114,6 +120,9 @@ class MatterLock(MatterEntity, LockEntity):
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
self.async_write_ha_state()
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
@ -135,26 +144,23 @@ class MatterLock(MatterEntity, LockEntity):
clusters.DoorLock.Attributes.LockState
)
# always reset the optimisically (un)locking state on state update
self._attr_is_locking = False
self._attr_is_unlocking = False
LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
self._attr_is_locked = True
self._attr_is_locking = False
self._attr_is_unlocking = False
elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked:
elif lock_state in (
clusters.DoorLock.Enums.DlLockState.kUnlocked,
clusters.DoorLock.Enums.DlLockState.kUnlatched,
clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
):
self._attr_is_locked = False
self._attr_is_locking = False
self._attr_is_unlocking = False
elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked:
if self.is_locked is True:
self._attr_is_unlocking = True
elif self.is_locked is False:
self._attr_is_locking = True
else:
# According to the matter docs a null state can happen during device startup.
self._attr_is_locked = None
self._attr_is_locking = None
self._attr_is_unlocking = None
if self.supports_door_position_sensor:
door_state = self.get_matter_attribute_value(

View File

@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [
device_types.ColorTemperatureLight,
device_types.DimmableLight,
device_types.ExtendedColorLight,
device_types.DimmerSwitch,
device_types.ColorDimmerSwitch,
device_types.OnOffLight,
device_types.AirPurifier,

View File

@ -44,5 +44,95 @@
"title": "[%key:component::random::config::step::sensor::title%]"
}
}
},
"selector": {
"binary_sensor_device_class": {
"options": {
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
"tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
}
},
"sensor_device_class": {
"options": {
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
"battery": "[%key:component::sensor::entity_component::battery::name%]",
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
"current": "[%key:component::sensor::entity_component::current::name%]",
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
"date": "[%key:component::sensor::entity_component::date::name%]",
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
"ph": "[%key:component::sensor::entity_component::ph::name%]",
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
"power": "[%key:component::sensor::entity_component::power::name%]",
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
"speed": "[%key:component::sensor::entity_component::speed::name%]",
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
}
}
}

View File

@ -960,14 +960,18 @@ RPC_SENSORS: Final = {
name="Analog input",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _, key: (
config[key]["type"] != "analog" or config[key]["enable"] is False
),
),
"analoginput_xpercent": RpcSensorDescription(
key="input",
sub_key="xpercent",
name="Analog value",
removal_condition=lambda config, status, key: (
config[key]["enable"] is False or status[key].get("xpercent") is None
config[key]["type"] != "analog"
or config[key]["enable"] is False
or status[key].get("xpercent") is None
),
),
"pulse_counter": RpcSensorDescription(
@ -977,7 +981,9 @@ RPC_SENSORS: Final = {
native_unit_of_measurement="pulse",
state_class=SensorStateClass.TOTAL,
value=lambda status, _: status["total"],
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _status, key: (
config[key]["type"] != "count" or config[key]["enable"] is False
),
),
"counter_value": RpcSensorDescription(
key="input",
@ -985,26 +991,29 @@ RPC_SENSORS: Final = {
name="Counter value",
value=lambda status, _: status["xtotal"],
removal_condition=lambda config, status, key: (
config[key]["enable"] is False
config[key]["type"] != "count"
or config[key]["enable"] is False
or status[key]["counts"].get("xtotal") is None
),
),
"counter_frequency": RpcSensorDescription(
key="input",
sub_key="counts",
sub_key="freq",
name="Pulse counter frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
value=lambda status, _: status["freq"],
removal_condition=lambda config, status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _, key: (
config[key]["type"] != "count" or config[key]["enable"] is False
),
),
"counter_frequency_value": RpcSensorDescription(
key="input",
sub_key="counts",
sub_key="xfreq",
name="Pulse counter frequency value",
value=lambda status, _: status["xfreq"],
removal_condition=lambda config, status, key: (
config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None
config[key]["type"] != "count"
or config[key]["enable"] is False
or status[key].get("xfreq") is None
),
),
}

View File

@ -9,7 +9,7 @@ from typing import Any
from starline import StarlineApi, StarlineDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
@ -65,9 +65,9 @@ class StarlineAccount:
)
self._api.set_slnet_token(slnet_token)
self._api.set_user_id(user_id)
self._hass.config_entries.async_update_entry(
self._config_entry,
data={
self._hass.add_job(
self._save_slnet_token,
{
**self._config_entry.data,
DATA_SLNET_TOKEN: slnet_token,
DATA_EXPIRES: slnet_token_expires,
@ -77,6 +77,13 @@ class StarlineAccount:
except Exception as err: # noqa: BLE001
_LOGGER.error("Error updating SLNet token: %s", err)
@callback
def _save_slnet_token(self, data) -> None:
self._hass.config_entries.async_update_entry(
self._config_entry,
data=data,
)
def _update_data(self):
"""Update StarLine data."""
self._check_slnet_token(self._update_interval)

View File

@ -48,6 +48,14 @@ class StreamWorkerError(Exception):
"""An exception thrown while processing a stream."""
def redact_av_error_string(err: av.AVError) -> str:
"""Return an error string with credentials redacted from the url."""
parts = [str(err.type), err.strerror]
if err.filename is not None:
parts.append(redact_credentials(err.filename))
return ", ".join(parts)
class StreamEndedError(StreamWorkerError):
"""Raised when the stream is complete, exposed for facilitating testing."""
@ -517,8 +525,7 @@ def stream_worker(
container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT)
except av.AVError as err:
raise StreamWorkerError(
f"Error opening stream ({err.type}, {err.strerror})"
f" {redact_credentials(str(source))}"
f"Error opening stream ({redact_av_error_string(err)})"
) from err
try:
video_stream = container.streams.video[0]
@ -593,7 +600,7 @@ def stream_worker(
except av.AVError as ex:
container.close()
raise StreamWorkerError(
f"Error demuxing stream while finding first packet: {ex!s}"
f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})"
) from ex
muxer = StreamMuxer(
@ -618,7 +625,9 @@ def stream_worker(
except StopIteration as ex:
raise StreamEndedError("Stream ended; no additional packets") from ex
except av.AVError as ex:
raise StreamWorkerError(f"Error demuxing stream: {ex!s}") from ex
raise StreamWorkerError(
f"Error demuxing stream ({redact_av_error_string(ex)})"
) from ex
muxer.mux_packet(packet)

View File

@ -382,7 +382,12 @@ class TPLinkLightEffectEntity(TPLinkLightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
brightness, transition = self._async_extract_brightness_transition(**kwargs)
if ATTR_EFFECT in kwargs:
if (
(effect := kwargs.get(ATTR_EFFECT))
# Effect is unlikely to be LIGHT_EFFECTS_OFF but check for it anyway
and effect not in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF}
and effect in self._effect_module.effect_list
):
await self._effect_module.set_effect(
kwargs[ATTR_EFFECT], brightness=brightness, transition=transition
)

View File

@ -297,5 +297,5 @@
"iot_class": "local_polling",
"loggers": ["kasa"],
"quality_scale": "platinum",
"requirements": ["python-kasa[speedups]==0.7.0.2"]
"requirements": ["python-kasa[speedups]==0.7.0.3"]
}

View File

@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
"requirements": ["velbus-aio==2024.5.1"],
"requirements": ["velbus-aio==2024.7.5"],
"usb": [
{
"vid": "10CF",

View File

@ -7,7 +7,7 @@
"iot_class": "local_push",
"loggers": ["aiowebostv"],
"quality_scale": "platinum",
"requirements": ["aiowebostv==0.4.1"],
"requirements": ["aiowebostv==0.4.2"],
"ssdp": [
{
"st": "urn:lge-com:service:webos-second-screen:1"

View File

@ -239,7 +239,8 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity):
self._attr_assumed_state = True
if (
self._client.media_state is not None
self._client.is_on
and self._client.media_state is not None
and self._client.media_state.get("foregroundAppInfo") is not None
):
self._attr_assumed_state = False

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.51"]
"requirements": ["holidays==0.52"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/youless",
"iot_class": "local_polling",
"loggers": ["youless_api"],
"requirements": ["youless-api==2.1.0"]
"requirements": ["youless-api==2.1.2"]
}

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -32,7 +32,7 @@ habluetooth==3.1.3
hass-nabucasa==0.81.1
hassil==1.7.1
home-assistant-bluetooth==1.12.2
home-assistant-frontend==20240703.0
home-assistant-frontend==20240705.0
home-assistant-intents==2024.7.3
httpx==0.27.0
ifaddr==0.2.0

View File

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

View File

@ -195,7 +195,7 @@ aioambient==2024.01.0
aioapcaccess==0.4.2
# homeassistant.components.aquacell
aioaquacell==0.1.7
aioaquacell==0.1.8
# homeassistant.components.aseko_pool_live
aioaseko==0.1.1
@ -404,7 +404,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webostv
aiowebostv==0.4.1
aiowebostv==0.4.2
# homeassistant.components.withings
aiowithings==3.0.2
@ -449,7 +449,7 @@ androidtvremote2==0.1.1
anel-pwrctrl-homeassistant==0.0.1.dev2
# homeassistant.components.anova
anova-wifi==0.14.0
anova-wifi==0.15.0
# homeassistant.components.anthemav
anthemav==1.4.1
@ -709,7 +709,7 @@ debugpy==1.8.1
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==8.1.0
deebot-client==8.1.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -1087,10 +1087,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.51
holidays==0.52
# homeassistant.components.frontend
home-assistant-frontend==20240703.0
home-assistant-frontend==20240705.0
# homeassistant.components.conversation
home-assistant-intents==2024.7.3
@ -1161,7 +1161,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.inkbird
inkbird-ble==0.5.7
inkbird-ble==0.5.8
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.5.0
@ -2275,7 +2275,7 @@ python-join-api==0.0.9
python-juicenet==1.1.0
# homeassistant.components.tplink
python-kasa[speedups]==0.7.0.2
python-kasa[speedups]==0.7.0.3
# homeassistant.components.lirc
# python-lirc==1.2.3
@ -2830,7 +2830,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.1
# homeassistant.components.velbus
velbus-aio==2024.5.1
velbus-aio==2024.7.5
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -2945,7 +2945,7 @@ yeelightsunflower==0.0.10
yolink-api==0.4.4
# homeassistant.components.youless
youless-api==2.1.0
youless-api==2.1.2
# homeassistant.components.youtube
youtubeaio==1.1.5

View File

@ -174,7 +174,7 @@ aioambient==2024.01.0
aioapcaccess==0.4.2
# homeassistant.components.aquacell
aioaquacell==0.1.7
aioaquacell==0.1.8
# homeassistant.components.aseko_pool_live
aioaseko==0.1.1
@ -377,7 +377,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webostv
aiowebostv==0.4.1
aiowebostv==0.4.2
# homeassistant.components.withings
aiowithings==3.0.2
@ -413,7 +413,7 @@ androidtv[async]==0.0.73
androidtvremote2==0.1.1
# homeassistant.components.anova
anova-wifi==0.14.0
anova-wifi==0.15.0
# homeassistant.components.anthemav
anthemav==1.4.1
@ -590,7 +590,7 @@ dbus-fast==2.22.1
debugpy==1.8.1
# homeassistant.components.ecovacs
deebot-client==8.1.0
deebot-client==8.1.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -892,10 +892,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.51
holidays==0.52
# homeassistant.components.frontend
home-assistant-frontend==20240703.0
home-assistant-frontend==20240705.0
# homeassistant.components.conversation
home-assistant-intents==2024.7.3
@ -951,7 +951,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.inkbird
inkbird-ble==0.5.7
inkbird-ble==0.5.8
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.5.0
@ -1775,7 +1775,7 @@ python-izone==1.2.9
python-juicenet==1.1.0
# homeassistant.components.tplink
python-kasa[speedups]==0.7.0.2
python-kasa[speedups]==0.7.0.3
# homeassistant.components.matter
python-matter-server==6.2.2
@ -2204,7 +2204,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.1
# homeassistant.components.velbus
velbus-aio==2024.5.1
velbus-aio==2024.7.5
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -2301,7 +2301,7 @@ yeelight==0.7.14
yolink-api==0.4.4
# homeassistant.components.youless
youless-api==2.1.0
youless-api==2.1.2
# homeassistant.components.youtube
youtubeaio==1.1.5

View File

@ -7757,6 +7757,141 @@
"serializedGlobalTradeItemNumber": "3014F711000000000ESIIEC2",
"type": "ENERGY_SENSORS_INTERFACE",
"updateState": "UP_TO_DATE"
},
"3014F7110000000000ESIIE3": {
"availableFirmwareVersion": "0.0.0",
"connectionType": "HMIP_RF",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.6",
"firmwareVersionInteger": 65542,
"functionalChannels": {
"0": {
"busConfigMismatch": null,
"coProFaulty": false,
"coProRestartNeeded": false,
"coProUpdateFailure": false,
"configPending": false,
"controlsMountingOrientation": null,
"daliBusState": null,
"defaultLinkedGroup": [],
"deviceCommunicationError": null,
"deviceDriveError": null,
"deviceDriveModeError": null,
"deviceId": "3014F7110000000000ESIIE3",
"deviceOperationMode": null,
"deviceOverheated": false,
"deviceOverloaded": false,
"devicePowerFailureDetected": false,
"deviceUndervoltage": false,
"displayContrast": null,
"dutyCycle": false,
"functionalChannelType": "DEVICE_BASE",
"groupIndex": 0,
"groups": ["00000000-0000-0000-0000-000000000031"],
"index": 0,
"label": "",
"lockJammed": null,
"lowBat": false,
"mountingOrientation": null,
"multicastRoutingEnabled": false,
"particulateMatterSensorCommunicationError": null,
"particulateMatterSensorError": null,
"powerShortCircuit": null,
"profilePeriodLimitReached": null,
"routerModuleEnabled": false,
"routerModuleSupported": false,
"rssiDeviceValue": -94,
"rssiPeerValue": null,
"sensorCommunicationError": false,
"sensorError": true,
"shortCircuitDataLine": null,
"supportedOptionalFeatures": {
"IFeatureBusConfigMismatch": false,
"IFeatureDeviceCoProError": false,
"IFeatureDeviceCoProRestart": false,
"IFeatureDeviceCoProUpdate": false,
"IFeatureDeviceCommunicationError": false,
"IFeatureDeviceDaliBusError": false,
"IFeatureDeviceDriveError": false,
"IFeatureDeviceDriveModeError": false,
"IFeatureDeviceIdentify": false,
"IFeatureDeviceOverheated": false,
"IFeatureDeviceOverloaded": false,
"IFeatureDeviceParticulateMatterSensorCommunicationError": false,
"IFeatureDeviceParticulateMatterSensorError": false,
"IFeatureDevicePowerFailure": false,
"IFeatureDeviceSensorCommunicationError": true,
"IFeatureDeviceSensorError": true,
"IFeatureDeviceTemperatureHumiditySensorCommunicationError": false,
"IFeatureDeviceTemperatureHumiditySensorError": false,
"IFeatureDeviceTemperatureOutOfRange": false,
"IFeatureDeviceUndervoltage": false,
"IFeatureMulticastRouter": false,
"IFeaturePowerShortCircuit": false,
"IFeatureProfilePeriodLimit": false,
"IFeatureRssiValue": true,
"IFeatureShortCircuitDataLine": false,
"IOptionalFeatureDefaultLinkedGroup": false,
"IOptionalFeatureDeviceErrorLockJammed": false,
"IOptionalFeatureDeviceOperationMode": false,
"IOptionalFeatureDisplayContrast": false,
"IOptionalFeatureDutyCycle": true,
"IOptionalFeatureLowBat": true,
"IOptionalFeatureMountingOrientation": false
},
"temperatureHumiditySensorCommunicationError": null,
"temperatureHumiditySensorError": null,
"temperatureOutOfRange": false,
"unreach": false
},
"1": {
"channelRole": "ENERGY_SENSOR",
"connectedEnergySensorType": "ES_LED",
"currentGasFlow": null,
"currentPowerConsumption": 189.15,
"deviceId": "3014F7110000000000ESIIE3",
"energyCounterOne": 23825.748,
"energyCounterOneType": "UNKNOWN",
"energyCounterThree": null,
"energyCounterThreeType": "UNKNOWN",
"energyCounterTwo": null,
"energyCounterTwoType": "UNKNOWN",
"functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL",
"gasVolume": null,
"gasVolumePerImpulse": 0.01,
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000057"],
"impulsesPerKWH": 1000,
"index": 1,
"label": "",
"supportedOptionalFeatures": {
"IOptionalFeatureCounterOffset": true,
"IOptionalFeatureCurrentGasFlow": false,
"IOptionalFeatureCurrentPowerConsumption": true,
"IOptionalFeatureEnergyCounterOne": true,
"IOptionalFeatureEnergyCounterThree": false,
"IOptionalFeatureEnergyCounterTwo": false,
"IOptionalFeatureGasVolume": false,
"IOptionalFeatureGasVolumePerImpulse": false,
"IOptionalFeatureImpulsesPerKWH": true
}
}
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000ESIIE3",
"label": "esi_led",
"lastStatusUpdate": 1702420986697,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manuallyUpdateForced": false,
"manufacturerCode": 1,
"measuredAttributes": {},
"modelId": 509,
"modelType": "HmIP-ESI",
"oem": "eQ-3",
"permanentlyReachable": false,
"serializedGlobalTradeItemNumber": "3014F7110000000000ESIIE3",
"type": "ENERGY_SENSORS_INTERFACE",
"updateState": "UP_TO_DATE"
}
},
"groups": {

View File

@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices(
test_devices=None, test_groups=None
)
assert len(mock_hap.hmip_device_by_entity_id) == 290
assert len(mock_hap.hmip_device_by_entity_id) == 293
async def test_hmip_remove_device(

View File

@ -634,3 +634,39 @@ async def test_hmip_esi_gas_gas_volume(
)
assert ha_state.state == "1019.26"
async def test_hmip_esi_led_current_power_consumption(
hass: HomeAssistant, default_mock_hap_factory
) -> None:
"""Test ESI-IEC currentPowerConsumption Sensor."""
entity_id = "sensor.esi_led_currentPowerConsumption"
entity_name = "esi_led CurrentPowerConsumption"
device_model = "HmIP-ESI"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["esi_led"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "189.15"
async def test_hmip_esi_led_energy_counter_usage_high_tariff(
hass: HomeAssistant, default_mock_hap_factory
) -> None:
"""Test ESI-IEC ENERGY_COUNTER_USAGE_HIGH_TARIFF."""
entity_id = "sensor.esi_led_energy_counter_usage_high_tariff"
entity_name = "esi_led ENERGY_COUNTER_USAGE_HIGH_TARIFF"
device_model = "HmIP-ESI"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["esi_led"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "23825.748"

View File

@ -1059,6 +1059,7 @@
'Front lawn',
'Back lawn',
'my_lawn',
'no_work_area_active',
]),
}),
'config_entry_id': <ANY>,
@ -1097,6 +1098,7 @@
'Front lawn',
'Back lawn',
'my_lawn',
'no_work_area_active',
]),
}),
'context': <ANY>,

View File

@ -87,6 +87,38 @@ async def test_next_start_sensor(
assert state.state == STATE_UNKNOWN
async def test_work_area_sensor(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the work area sensor."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("sensor.test_mower_1_work_area")
assert state is not None
assert state.state == "Front lawn"
values = mower_list_to_dictionary_dataclass(
load_json_value_fixture("mower.json", DOMAIN)
)
values[TEST_MOWER_ID].mower.work_area_id = None
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_work_area")
assert state.state == "no_work_area_active"
values[TEST_MOWER_ID].mower.work_area_id = 0
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_work_area")
assert state.state == "my_lawn"
@pytest.mark.parametrize(
("sensor_to_test"),
[

View File

@ -8,13 +8,11 @@ import pytest
from homeassistant.components.lock import (
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_UNLOCKED,
STATE_UNLOCKING,
LockEntityFeature,
)
from homeassistant.const import ATTR_CODE, STATE_UNKNOWN
from homeassistant.const import ATTR_CODE, STATE_LOCKING, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.entity_registry as er
@ -68,14 +66,14 @@ async def test_lock(
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_LOCKED
assert state.state == STATE_LOCKING
set_node_attribute(door_lock, 1, 257, 0, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_UNLOCKING
assert state.state == STATE_UNLOCKED
set_node_attribute(door_lock, 1, 257, 0, 2)
await trigger_subscription_callback(hass, matter_client)
@ -89,7 +87,7 @@ async def test_lock(
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_LOCKING
assert state.state == STATE_UNLOCKED
set_node_attribute(door_lock, 1, 257, 0, None)
await trigger_subscription_callback(hass, matter_client)

View File

@ -228,7 +228,9 @@ MOCK_STATUS_RPC = {
"input:1": {"id": 1, "percent": 89, "xpercent": 8.9},
"input:2": {
"id": 2,
"counts": {"total": 56174, "xtotal": 561.74, "freq": 208.00, "xfreq": 6.11},
"counts": {"total": 56174, "xtotal": 561.74},
"freq": 208.00,
"xfreq": 6.11,
},
"light:0": {"output": True, "brightness": 53.0},
"light:1": {"output": True, "brightness": 53.0},

View File

@ -828,3 +828,29 @@ async def test_rpc_pulse_counter_frequency_sensors(
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.unique_id == "123456789ABC-input:2-counter_frequency_value"
async def test_rpc_disabled_xfreq(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test RPC input with the xfreq sensor disabled."""
status = deepcopy(mock_rpc_device.status)
status["input:2"] = {
"id": 2,
"counts": {"total": 56174, "xtotal": 561.74},
"freq": 208.00,
}
monkeypatch.setattr(mock_rpc_device, "status", status)
await init_integration(hass, 2)
entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value"
state = hass.states.get(entity_id)
assert not state
entry = entity_registry.async_get(entity_id)
assert not entry

View File

@ -772,12 +772,15 @@ async def test_worker_log(
with patch("av.open") as av_open:
# pylint: disable-next=c-extension-no-member
av_open.side_effect = av.error.InvalidDataError(-2, "error")
av_open.side_effect = av.error.InvalidDataError(
code=-2, message="Invalid data", filename=stream_url
)
with pytest.raises(StreamWorkerError) as err:
run_worker(hass, stream, stream_url)
await hass.async_block_till_done()
assert (
str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}"
str(err.value)
== f"Error opening stream (ERRORTYPE_-2, Invalid data, {redacted_url})"
)
assert stream_url not in caplog.text

View File

@ -210,7 +210,8 @@ def _mocked_device(
if modules:
device.modules = {
module_name: MODULE_TO_MOCK_GEN[module_name]() for module_name in modules
module_name: MODULE_TO_MOCK_GEN[module_name](device)
for module_name in modules
}
if features:
@ -298,7 +299,7 @@ def _mocked_feature(
return feature
def _mocked_light_module() -> Light:
def _mocked_light_module(device) -> Light:
light = MagicMock(spec=Light, name="Mocked light module")
light.update = AsyncMock()
light.brightness = 50
@ -314,26 +315,58 @@ def _mocked_light_module() -> Light:
light.hsv = (10, 30, 5)
light.valid_temperature_range = ColorTempRange(min=4000, max=9000)
light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
light.set_state = AsyncMock()
light.set_brightness = AsyncMock()
light.set_hsv = AsyncMock()
light.set_color_temp = AsyncMock()
async def _set_state(state, *_, **__):
light.state = state
light.set_state = AsyncMock(wraps=_set_state)
async def _set_brightness(brightness, *_, **__):
light.state.brightness = brightness
light.state.light_on = brightness > 0
light.set_brightness = AsyncMock(wraps=_set_brightness)
async def _set_hsv(h, s, v, *_, **__):
light.state.hue = h
light.state.saturation = s
light.state.brightness = v
light.state.light_on = True
light.set_hsv = AsyncMock(wraps=_set_hsv)
async def _set_color_temp(temp, *_, **__):
light.state.color_temp = temp
light.state.light_on = True
light.set_color_temp = AsyncMock(wraps=_set_color_temp)
light.protocol = _mock_protocol()
return light
def _mocked_light_effect_module() -> LightEffect:
def _mocked_light_effect_module(device) -> LightEffect:
effect = MagicMock(spec=LightEffect, name="Mocked light effect")
effect.has_effects = True
effect.has_custom_effects = True
effect.effect = "Effect1"
effect.effect_list = ["Off", "Effect1", "Effect2"]
effect.set_effect = AsyncMock()
async def _set_effect(effect_name, *_, **__):
assert (
effect_name in effect.effect_list
), f"set_effect '{effect_name}' not in {effect.effect_list}"
assert device.modules[
Module.Light
], "Need a light module to test set_effect method"
device.modules[Module.Light].state.light_on = True
effect.effect = effect_name
effect.set_effect = AsyncMock(wraps=_set_effect)
effect.set_custom_effect = AsyncMock()
return effect
def _mocked_fan_module() -> Fan:
def _mocked_fan_module(effect) -> Fan:
fan = MagicMock(auto_spec=Fan, name="Mocked fan")
fan.fan_speed_level = 0
fan.set_fan_speed_level = AsyncMock()

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock
from freezegun.api import FrozenDateTimeFactory
from kasa import (
AuthenticationError,
DeviceType,
@ -36,7 +37,13 @@ from homeassistant.components.light import (
)
from homeassistant.components.tplink.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@ -920,3 +927,82 @@ async def test_light_child(
assert child_entity
assert child_entity.unique_id == f"{DEVICE_ID}0{light_id}"
assert child_entity.device_id == entity.device_id
async def test_scene_effect_light(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test activating a scene works with effects.
i.e. doesn't try to set the effect to 'off'
"""
already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
)
already_migrated_config_entry.add_to_hass(hass)
device = _mocked_device(
modules=[Module.Light, Module.LightEffect], alias="my_light"
)
light_effect = device.modules[Module.LightEffect]
light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF
with _patch_discovery(device=device), _patch_connect(device=device):
assert await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
assert await async_setup_component(hass, "scene", {})
await hass.async_block_till_done()
entity_id = "light.my_light"
await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.async_block_till_done()
freezer.tick(5)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state is STATE_ON
assert state.attributes["effect"] is EFFECT_OFF
await hass.services.async_call(
"scene",
"create",
{"scene_id": "effect_off_scene", "snapshot_entities": [entity_id]},
blocking=True,
)
await hass.async_block_till_done()
scene_state = hass.states.get("scene.effect_off_scene")
assert scene_state.state is STATE_UNKNOWN
await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.async_block_till_done()
freezer.tick(5)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state is STATE_OFF
await hass.services.async_call(
"scene",
"turn_on",
{
"entity_id": "scene.effect_off_scene",
},
blocking=True,
)
await hass.async_block_till_done()
scene_state = hass.states.get("scene.effect_off_scene")
assert scene_state.state is not STATE_UNKNOWN
freezer.tick(5)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state is STATE_ON
assert state.attributes["effect"] is EFFECT_OFF

View File

@ -832,3 +832,7 @@ async def test_update_media_state(hass: HomeAssistant, client, monkeypatch) -> N
monkeypatch.setattr(client, "media_state", data)
await client.mock_state_update()
assert hass.states.get(ENTITY_ID).state == MediaPlayerState.IDLE
monkeypatch.setattr(client, "is_on", False)
await client.mock_state_update()
assert hass.states.get(ENTITY_ID).state == STATE_OFF

View File

@ -47,7 +47,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_delivery_low-entry]
@ -98,7 +98,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
'state': '0.029',
})
# ---
# name: test_sensors[sensor.energy_high-entry]
@ -405,7 +405,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1234.564',
'state': '1624.264',
})
# ---
# name: test_sensors[sensor.phase_1_current-entry]
@ -967,6 +967,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
'state': '1234.564',
})
# ---