mirror of
https://github.com/home-assistant/core.git
synced 2025-11-19 07:50:12 +00:00
Compare commits
15 Commits
add-includ
...
context-tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9767aa5e9b | ||
|
|
b4eb73be98 | ||
|
|
0ac3f776fa | ||
|
|
8e8a4fff11 | ||
|
|
579ffcc64d | ||
|
|
81943fb31d | ||
|
|
70dd0bf12e | ||
|
|
c2d462c1e7 | ||
|
|
49e050cc60 | ||
|
|
f6d829a2f3 | ||
|
|
e44e3b6f25 | ||
|
|
af603661c0 | ||
|
|
35c6113777 | ||
|
|
3c2f729ddc | ||
|
|
0d63cb765f |
@@ -386,11 +386,260 @@ def _async_validate_auto_generated_cost_entity(
|
||||
issues.add_issue(hass, "recorder_untracked", cost_entity_id)
|
||||
|
||||
|
||||
def _validate_grid_source(
|
||||
hass: HomeAssistant,
|
||||
source: data.GridSourceType,
|
||||
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||
wanted_statistics_metadata: set[str],
|
||||
source_result: ValidationIssues,
|
||||
validate_calls: list[functools.partial[None]],
|
||||
) -> None:
|
||||
"""Validate grid energy source."""
|
||||
flow_from: data.FlowFromGridSourceType
|
||||
for flow_from in source["flow_from"]:
|
||||
wanted_statistics_metadata.add(flow_from["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
flow_from["stat_energy_from"],
|
||||
ENERGY_USAGE_DEVICE_CLASSES,
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_cost := flow_from.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := flow_from.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
ENERGY_PRICE_UNITS,
|
||||
ENERGY_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
flow_from.get("entity_energy_price") is not None
|
||||
or flow_from.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
flow_from["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
flow_to: data.FlowToGridSourceType
|
||||
for flow_to in source["flow_to"]:
|
||||
wanted_statistics_metadata.add(flow_to["stat_energy_to"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
flow_to["stat_energy_to"],
|
||||
ENERGY_USAGE_DEVICE_CLASSES,
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_compensation := flow_to.get("stat_compensation")) is not None:
|
||||
wanted_statistics_metadata.add(stat_compensation)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_compensation,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := flow_to.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
ENERGY_PRICE_UNITS,
|
||||
ENERGY_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
flow_to.get("entity_energy_price") is not None
|
||||
or flow_to.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
flow_to["stat_energy_to"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
for power_stat in source.get("power", []):
|
||||
wanted_statistics_metadata.add(power_stat["stat_rate"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_power_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
power_stat["stat_rate"],
|
||||
POWER_USAGE_DEVICE_CLASSES,
|
||||
POWER_USAGE_UNITS,
|
||||
POWER_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _validate_gas_source(
|
||||
hass: HomeAssistant,
|
||||
source: data.GasSourceType,
|
||||
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||
wanted_statistics_metadata: set[str],
|
||||
source_result: ValidationIssues,
|
||||
validate_calls: list[functools.partial[None]],
|
||||
) -> None:
|
||||
"""Validate gas energy source."""
|
||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
source["stat_energy_from"],
|
||||
GAS_USAGE_DEVICE_CLASSES,
|
||||
GAS_USAGE_UNITS,
|
||||
GAS_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_cost := source.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
GAS_PRICE_UNITS,
|
||||
GAS_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
source.get("entity_energy_price") is not None
|
||||
or source.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _validate_water_source(
|
||||
hass: HomeAssistant,
|
||||
source: data.WaterSourceType,
|
||||
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||
wanted_statistics_metadata: set[str],
|
||||
source_result: ValidationIssues,
|
||||
validate_calls: list[functools.partial[None]],
|
||||
) -> None:
|
||||
"""Validate water energy source."""
|
||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
source["stat_energy_from"],
|
||||
WATER_USAGE_DEVICE_CLASSES,
|
||||
WATER_USAGE_UNITS,
|
||||
WATER_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_cost := source.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
WATER_PRICE_UNITS,
|
||||
WATER_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
source.get("entity_energy_price") is not None
|
||||
or source.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
"""Validate the energy configuration."""
|
||||
manager: data.EnergyManager = await data.async_get_manager(hass)
|
||||
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {}
|
||||
validate_calls = []
|
||||
validate_calls: list[functools.partial[None]] = []
|
||||
wanted_statistics_metadata: set[str] = set()
|
||||
|
||||
result = EnergyPreferencesValidation()
|
||||
@@ -404,230 +653,35 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
result.energy_sources.append(source_result)
|
||||
|
||||
if source["type"] == "grid":
|
||||
flow: data.FlowFromGridSourceType | data.FlowToGridSourceType
|
||||
for flow in source["flow_from"]:
|
||||
wanted_statistics_metadata.add(flow["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
flow["stat_energy_from"],
|
||||
ENERGY_USAGE_DEVICE_CLASSES,
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_cost := flow.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (
|
||||
entity_energy_price := flow.get("entity_energy_price")
|
||||
) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
ENERGY_PRICE_UNITS,
|
||||
ENERGY_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
flow.get("entity_energy_price") is not None
|
||||
or flow.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
flow["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
for flow in source["flow_to"]:
|
||||
wanted_statistics_metadata.add(flow["stat_energy_to"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
flow["stat_energy_to"],
|
||||
ENERGY_USAGE_DEVICE_CLASSES,
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
if (stat_compensation := flow.get("stat_compensation")) is not None:
|
||||
wanted_statistics_metadata.add(stat_compensation)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_compensation,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (
|
||||
entity_energy_price := flow.get("entity_energy_price")
|
||||
) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
ENERGY_PRICE_UNITS,
|
||||
ENERGY_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
flow.get("entity_energy_price") is not None
|
||||
or flow.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
flow["stat_energy_to"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
for power_stat in source.get("power", []):
|
||||
wanted_statistics_metadata.add(power_stat["stat_rate"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_power_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
power_stat["stat_rate"],
|
||||
POWER_USAGE_DEVICE_CLASSES,
|
||||
POWER_USAGE_UNITS,
|
||||
POWER_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
_validate_grid_source(
|
||||
hass,
|
||||
source,
|
||||
statistics_metadata,
|
||||
wanted_statistics_metadata,
|
||||
source_result,
|
||||
validate_calls,
|
||||
)
|
||||
|
||||
elif source["type"] == "gas":
|
||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
source["stat_energy_from"],
|
||||
GAS_USAGE_DEVICE_CLASSES,
|
||||
GAS_USAGE_UNITS,
|
||||
GAS_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
_validate_gas_source(
|
||||
hass,
|
||||
source,
|
||||
statistics_metadata,
|
||||
wanted_statistics_metadata,
|
||||
source_result,
|
||||
validate_calls,
|
||||
)
|
||||
|
||||
if (stat_cost := source.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
GAS_PRICE_UNITS,
|
||||
GAS_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
source.get("entity_energy_price") is not None
|
||||
or source.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
elif source["type"] == "water":
|
||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_usage_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
source["stat_energy_from"],
|
||||
WATER_USAGE_DEVICE_CLASSES,
|
||||
WATER_USAGE_UNITS,
|
||||
WATER_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
_validate_water_source(
|
||||
hass,
|
||||
source,
|
||||
statistics_metadata,
|
||||
wanted_statistics_metadata,
|
||||
source_result,
|
||||
validate_calls,
|
||||
)
|
||||
|
||||
if (stat_cost := source.get("stat_cost")) is not None:
|
||||
wanted_statistics_metadata.add(stat_cost)
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_cost_stat,
|
||||
hass,
|
||||
statistics_metadata,
|
||||
stat_cost,
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_price_entity,
|
||||
hass,
|
||||
entity_energy_price,
|
||||
source_result,
|
||||
WATER_PRICE_UNITS,
|
||||
WATER_PRICE_UNIT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
source.get("entity_energy_price") is not None
|
||||
or source.get("number_energy_price") is not None
|
||||
):
|
||||
validate_calls.append(
|
||||
functools.partial(
|
||||
_async_validate_auto_generated_cost_entity,
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
source_result,
|
||||
)
|
||||
)
|
||||
|
||||
elif source["type"] == "solar":
|
||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||
validate_calls.append(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from urllib.parse import quote
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -152,7 +153,9 @@ class HassFoscamCamera(FoscamEntity, Camera):
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the stream source."""
|
||||
if self._rtsp_port:
|
||||
return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}"
|
||||
_username = quote(self._username)
|
||||
_password = quote(self._password)
|
||||
return f"rtsp://{_username}:{_password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_connect_zbt2",
|
||||
"integration_type": "hardware",
|
||||
"loggers": [
|
||||
"bellows",
|
||||
"universal_silabs_flasher",
|
||||
"zigpy.serial",
|
||||
"serial_asyncio_fast"
|
||||
],
|
||||
"quality_scale": "bronze",
|
||||
"usb": [
|
||||
{
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect",
|
||||
"integration_type": "hardware",
|
||||
"loggers": [
|
||||
"bellows",
|
||||
"universal_silabs_flasher",
|
||||
"zigpy.serial",
|
||||
"serial_asyncio_fast"
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*skyconnect v1.0*",
|
||||
|
||||
@@ -7,5 +7,11 @@
|
||||
"dependencies": ["hardware", "homeassistant_hardware"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow",
|
||||
"integration_type": "hardware",
|
||||
"loggers": [
|
||||
"bellows",
|
||||
"universal_silabs_flasher",
|
||||
"zigpy.serial",
|
||||
"serial_asyncio_fast"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.2"]
|
||||
"requirements": ["pylamarzocco==2.1.3"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Ruuvitag BLE device from a config entry."""
|
||||
"""Set up Ruuvi BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = RuuvitagBluetoothDeviceData()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "ruuvitag_ble",
|
||||
"name": "RuuviTag BLE",
|
||||
"name": "Ruuvi BLE",
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": false,
|
||||
|
||||
@@ -191,7 +191,7 @@ async def async_setup_entry(
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Ruuvitag BLE sensors."""
|
||||
"""Set up the Ruuvi BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
@@ -210,7 +210,7 @@ class RuuvitagBluetoothSensorEntity(
|
||||
],
|
||||
SensorEntity,
|
||||
):
|
||||
"""Representation of a Ruuvitag BLE sensor."""
|
||||
"""Representation of a Ruuvi BLE sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
|
||||
@@ -13,20 +13,19 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_ARM_HOME_MODE,
|
||||
CONF_PARTITION_NUMBER,
|
||||
DOMAIN,
|
||||
SIGNAL_PANEL_MESSAGE,
|
||||
SUBENTRY_TYPE_PARTITION,
|
||||
SatelConfigEntry,
|
||||
)
|
||||
from .entity import SatelIntegraEntity
|
||||
|
||||
ALARM_STATE_MAP = {
|
||||
AlarmState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||
@@ -59,54 +58,49 @@ async def async_setup_entry(
|
||||
|
||||
for subentry in partition_subentries:
|
||||
partition_num: int = subentry.data[CONF_PARTITION_NUMBER]
|
||||
zone_name: str = subentry.data[CONF_NAME]
|
||||
arm_home_mode: int = subentry.data[CONF_ARM_HOME_MODE]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SatelIntegraAlarmPanel(
|
||||
controller,
|
||||
zone_name,
|
||||
arm_home_mode,
|
||||
partition_num,
|
||||
config_entry.entry_id,
|
||||
subentry,
|
||||
partition_num,
|
||||
arm_home_mode,
|
||||
)
|
||||
],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
|
||||
class SatelIntegraAlarmPanel(SatelIntegraEntity, AlarmControlPanelEntity):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
_attr_code_format = CodeFormat.NUMBER
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = (
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AsyncSatel,
|
||||
device_name: str,
|
||||
arm_home_mode: int,
|
||||
partition_id: int,
|
||||
config_entry_id: str,
|
||||
subentry: ConfigSubentry,
|
||||
device_number: int,
|
||||
arm_home_mode: int,
|
||||
) -> None:
|
||||
"""Initialize the alarm panel."""
|
||||
self._attr_unique_id = f"{config_entry_id}_alarm_panel_{partition_id}"
|
||||
self._arm_home_mode = arm_home_mode
|
||||
self._partition_id = partition_id
|
||||
self._satel = controller
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device_name, identifiers={(DOMAIN, self._attr_unique_id)}
|
||||
super().__init__(
|
||||
controller,
|
||||
config_entry_id,
|
||||
subentry,
|
||||
device_number,
|
||||
)
|
||||
|
||||
self._arm_home_mode = arm_home_mode
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Update alarm status and register callbacks for future updates."""
|
||||
self._attr_alarm_state = self._read_alarm_state()
|
||||
@@ -136,7 +130,7 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
|
||||
for satel_state, ha_state in ALARM_STATE_MAP.items():
|
||||
if (
|
||||
satel_state in self._satel.partition_states
|
||||
and self._partition_id in self._satel.partition_states[satel_state]
|
||||
and self._device_number in self._satel.partition_states[satel_state]
|
||||
):
|
||||
return ha_state
|
||||
|
||||
@@ -152,21 +146,21 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
|
||||
self._attr_alarm_state == AlarmControlPanelState.TRIGGERED
|
||||
)
|
||||
|
||||
await self._satel.disarm(code, [self._partition_id])
|
||||
await self._satel.disarm(code, [self._device_number])
|
||||
|
||||
if clear_alarm_necessary:
|
||||
# Wait 1s before clearing the alarm
|
||||
await asyncio.sleep(1)
|
||||
await self._satel.clear_alarm(code, [self._partition_id])
|
||||
await self._satel.clear_alarm(code, [self._device_number])
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
|
||||
if code:
|
||||
await self._satel.arm(code, [self._partition_id])
|
||||
await self._satel.arm(code, [self._device_number])
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
|
||||
if code:
|
||||
await self._satel.arm(code, [self._partition_id], self._arm_home_mode)
|
||||
await self._satel.arm(code, [self._device_number], self._arm_home_mode)
|
||||
|
||||
@@ -8,25 +8,22 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_OUTPUT_NUMBER,
|
||||
CONF_OUTPUTS,
|
||||
CONF_ZONE_NUMBER,
|
||||
CONF_ZONE_TYPE,
|
||||
CONF_ZONES,
|
||||
DOMAIN,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
SUBENTRY_TYPE_OUTPUT,
|
||||
SUBENTRY_TYPE_ZONE,
|
||||
SatelConfigEntry,
|
||||
)
|
||||
from .entity import SatelIntegraEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -46,18 +43,16 @@ async def async_setup_entry(
|
||||
for subentry in zone_subentries:
|
||||
zone_num: int = subentry.data[CONF_ZONE_NUMBER]
|
||||
zone_type: BinarySensorDeviceClass = subentry.data[CONF_ZONE_TYPE]
|
||||
zone_name: str = subentry.data[CONF_NAME]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SatelIntegraBinarySensor(
|
||||
controller,
|
||||
zone_num,
|
||||
zone_name,
|
||||
zone_type,
|
||||
CONF_ZONES,
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
config_entry.entry_id,
|
||||
subentry,
|
||||
zone_num,
|
||||
zone_type,
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
)
|
||||
],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
@@ -71,51 +66,44 @@ async def async_setup_entry(
|
||||
for subentry in output_subentries:
|
||||
output_num: int = subentry.data[CONF_OUTPUT_NUMBER]
|
||||
ouput_type: BinarySensorDeviceClass = subentry.data[CONF_ZONE_TYPE]
|
||||
output_name: str = subentry.data[CONF_NAME]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SatelIntegraBinarySensor(
|
||||
controller,
|
||||
output_num,
|
||||
output_name,
|
||||
ouput_type,
|
||||
CONF_OUTPUTS,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
config_entry.entry_id,
|
||||
subentry,
|
||||
output_num,
|
||||
ouput_type,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
)
|
||||
],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class SatelIntegraBinarySensor(BinarySensorEntity):
|
||||
class SatelIntegraBinarySensor(SatelIntegraEntity, BinarySensorEntity):
|
||||
"""Representation of an Satel Integra binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AsyncSatel,
|
||||
device_number: int,
|
||||
device_name: str,
|
||||
device_class: BinarySensorDeviceClass,
|
||||
sensor_type: str,
|
||||
react_to_signal: str,
|
||||
config_entry_id: str,
|
||||
subentry: ConfigSubentry,
|
||||
device_number: int,
|
||||
device_class: BinarySensorDeviceClass,
|
||||
react_to_signal: str,
|
||||
) -> None:
|
||||
"""Initialize the binary_sensor."""
|
||||
self._device_number = device_number
|
||||
self._attr_unique_id = f"{config_entry_id}_{sensor_type}_{device_number}"
|
||||
self._react_to_signal = react_to_signal
|
||||
self._satel = controller
|
||||
super().__init__(
|
||||
controller,
|
||||
config_entry_id,
|
||||
subentry,
|
||||
device_number,
|
||||
)
|
||||
|
||||
self._attr_device_class = device_class
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device_name, identifiers={(DOMAIN, self._attr_unique_id)}
|
||||
)
|
||||
self._react_to_signal = react_to_signal
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
58
homeassistant/components/satel_integra/entity.py
Normal file
58
homeassistant/components/satel_integra/entity.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Satel Integra base entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from satel_integra.satel_integra import AsyncSatel
|
||||
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SUBENTRY_TYPE_OUTPUT,
|
||||
SUBENTRY_TYPE_PARTITION,
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
|
||||
SUBENTRY_TYPE_ZONE,
|
||||
)
|
||||
|
||||
SubentryTypeToEntityType: dict[str, str] = {
|
||||
SUBENTRY_TYPE_PARTITION: "alarm_panel",
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT: "switch",
|
||||
SUBENTRY_TYPE_ZONE: "zones",
|
||||
SUBENTRY_TYPE_OUTPUT: "outputs",
|
||||
}
|
||||
|
||||
|
||||
class SatelIntegraEntity(Entity):
|
||||
"""Defines a base Satel Integra entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AsyncSatel,
|
||||
config_entry_id: str,
|
||||
subentry: ConfigSubentry,
|
||||
device_number: int,
|
||||
) -> None:
|
||||
"""Initialize the Satel Integra entity."""
|
||||
|
||||
self._satel = controller
|
||||
self._device_number = device_number
|
||||
|
||||
entity_type = SubentryTypeToEntityType[subentry.subentry_type]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert entity_type is not None
|
||||
|
||||
self._attr_unique_id = f"{config_entry_id}_{entity_type}_{device_number}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=subentry.data[CONF_NAME], identifiers={(DOMAIN, self._attr_unique_id)}
|
||||
)
|
||||
@@ -7,19 +7,19 @@ from typing import Any
|
||||
from satel_integra.satel_integra import AsyncSatel
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import CONF_CODE, CONF_NAME
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
DOMAIN,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
|
||||
SatelConfigEntry,
|
||||
)
|
||||
from .entity import SatelIntegraEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -38,47 +38,42 @@ async def async_setup_entry(
|
||||
|
||||
for subentry in switchable_output_subentries:
|
||||
switchable_output_num: int = subentry.data[CONF_SWITCHABLE_OUTPUT_NUMBER]
|
||||
switchable_output_name: str = subentry.data[CONF_NAME]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SatelIntegraSwitch(
|
||||
controller,
|
||||
switchable_output_num,
|
||||
switchable_output_name,
|
||||
config_entry.options.get(CONF_CODE),
|
||||
config_entry.entry_id,
|
||||
subentry,
|
||||
switchable_output_num,
|
||||
config_entry.options.get(CONF_CODE),
|
||||
),
|
||||
],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class SatelIntegraSwitch(SwitchEntity):
|
||||
"""Representation of an Satel switch."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
class SatelIntegraSwitch(SatelIntegraEntity, SwitchEntity):
|
||||
"""Representation of an Satel Integra switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AsyncSatel,
|
||||
device_number: int,
|
||||
device_name: str,
|
||||
code: str | None,
|
||||
config_entry_id: str,
|
||||
subentry: ConfigSubentry,
|
||||
device_number: int,
|
||||
code: str | None,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
self._device_number = device_number
|
||||
self._attr_unique_id = f"{config_entry_id}_switch_{device_number}"
|
||||
self._code = code
|
||||
self._satel = controller
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device_name, identifiers={(DOMAIN, self._attr_unique_id)}
|
||||
super().__init__(
|
||||
controller,
|
||||
config_entry_id,
|
||||
subentry,
|
||||
device_number,
|
||||
)
|
||||
|
||||
self._code = code
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._attr_is_on = self._device_number in self._satel.violated_outputs
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
|
||||
from aiosenz import SENZAPI, Thermostat
|
||||
from httpx import RequestError
|
||||
import jwt
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
@@ -82,3 +83,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: SENZConfigEntry) -> bool
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SENZConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: SENZConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
# Use sub(ject) from access_token as unique_id
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
token = jwt.decode(
|
||||
config_entry.data["token"]["access_token"],
|
||||
options={"verify_signature": False},
|
||||
)
|
||||
uid = token["sub"]
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=uid, minor_version=2
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
import logging
|
||||
|
||||
import jwt
|
||||
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -12,6 +15,8 @@ class OAuth2FlowHandler(
|
||||
):
|
||||
"""Config flow to handle SENZ OAuth2 authentication."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
@property
|
||||
@@ -23,3 +28,15 @@ class OAuth2FlowHandler(
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {"scope": "restapi offline_access"}
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create or update the config entry."""
|
||||
|
||||
token = jwt.decode(
|
||||
data["token"]["access_token"], options={"verify_signature": False}
|
||||
)
|
||||
uid = token["sub"]
|
||||
await self.async_set_unique_id(uid)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
return await super().async_oauth_create_entry(data)
|
||||
|
||||
@@ -219,7 +219,6 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED]
|
||||
self._attr_code_format = config[CONF_CODE_FORMAT].value
|
||||
|
||||
self._state: AlarmControlPanelState | None = None
|
||||
self._attr_supported_features: AlarmControlPanelEntityFeature = (
|
||||
AlarmControlPanelEntityFeature(0)
|
||||
)
|
||||
@@ -244,11 +243,6 @@ class AbstractTemplateAlarmControlPanel(
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
yield (action_id, action_config, supported_feature)
|
||||
|
||||
@property
|
||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
async def _async_handle_restored_state(self) -> None:
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
@@ -256,14 +250,14 @@ class AbstractTemplateAlarmControlPanel(
|
||||
and last_state.state in _VALID_STATES
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and self._state is None
|
||||
and self._attr_alarm_state is None
|
||||
):
|
||||
self._state = AlarmControlPanelState(last_state.state)
|
||||
self._attr_alarm_state = AlarmControlPanelState(last_state.state)
|
||||
|
||||
def _handle_state(self, result: Any) -> None:
|
||||
# Validate state
|
||||
if result in _VALID_STATES:
|
||||
self._state = result
|
||||
self._attr_alarm_state = result
|
||||
_LOGGER.debug("Valid state - %s", result)
|
||||
return
|
||||
|
||||
@@ -273,7 +267,7 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
self._attr_alarm_state = None
|
||||
|
||||
async def _async_alarm_arm(self, state: Any, script: Script | None, code: Any):
|
||||
"""Arm the panel to specified state with supplied script."""
|
||||
@@ -284,7 +278,7 @@ class AbstractTemplateAlarmControlPanel(
|
||||
)
|
||||
|
||||
if self._attr_assumed_state:
|
||||
self._state = state
|
||||
self._attr_alarm_state = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
@@ -376,7 +370,7 @@ class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlP
|
||||
@callback
|
||||
def _update_state(self, result):
|
||||
if isinstance(result, TemplateError):
|
||||
self._state = None
|
||||
self._attr_alarm_state = None
|
||||
return
|
||||
|
||||
self._handle_state(result)
|
||||
@@ -386,7 +380,7 @@ class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlP
|
||||
"""Set up templates."""
|
||||
if self._template:
|
||||
self.add_template_attribute(
|
||||
"_state", self._template, None, self._update_state
|
||||
"_attr_alarm_state", self._template, None, self._update_state
|
||||
)
|
||||
super()._async_setup_templates()
|
||||
|
||||
|
||||
@@ -709,6 +709,7 @@ class DPCode(StrEnum):
|
||||
DEW_POINT_TEMP = "dew_point_temp"
|
||||
DISINFECTION = "disinfection"
|
||||
DO_NOT_DISTURB = "do_not_disturb"
|
||||
DOORBELL_PIC = "doorbell_pic"
|
||||
DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor
|
||||
DOORCONTACT_STATE_2 = "doorcontact_state_2"
|
||||
DOORCONTACT_STATE_3 = "doorcontact_state_3"
|
||||
|
||||
@@ -15,6 +15,13 @@ from homeassistant.util import dt as dt_util
|
||||
from . import TuyaConfigEntry
|
||||
from .const import DOMAIN, DPCode
|
||||
|
||||
_REDACTED_DPCODES = {
|
||||
DPCode.ALARM_MESSAGE,
|
||||
DPCode.ALARM_MSG,
|
||||
DPCode.DOORBELL_PIC,
|
||||
DPCode.MOVEMENT_DETECT_PIC,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: TuyaConfigEntry
|
||||
@@ -95,7 +102,7 @@ def _async_device_as_dict(
|
||||
# Gather Tuya states
|
||||
for dpcode, value in device.status.items():
|
||||
# These statuses may contain sensitive information, redact these..
|
||||
if dpcode in {DPCode.ALARM_MESSAGE, DPCode.MOVEMENT_DETECT_PIC}:
|
||||
if dpcode in _REDACTED_DPCODES:
|
||||
data["status"][dpcode] = REDACTED
|
||||
continue
|
||||
|
||||
|
||||
@@ -56,37 +56,32 @@ class VeluxCover(VeluxEntity, CoverEntity):
|
||||
def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
|
||||
"""Initialize VeluxCover."""
|
||||
super().__init__(node, config_entry_id)
|
||||
# Features common to all covers
|
||||
self._attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.STOP
|
||||
)
|
||||
# Window is the default device class for covers
|
||||
self._attr_device_class = CoverDeviceClass.WINDOW
|
||||
if isinstance(node, Awning):
|
||||
self._attr_device_class = CoverDeviceClass.AWNING
|
||||
if isinstance(node, Blind):
|
||||
self._attr_device_class = CoverDeviceClass.BLIND
|
||||
self._is_blind = True
|
||||
if isinstance(node, GarageDoor):
|
||||
self._attr_device_class = CoverDeviceClass.GARAGE
|
||||
if isinstance(node, Gate):
|
||||
self._attr_device_class = CoverDeviceClass.GATE
|
||||
if isinstance(node, RollerShutter):
|
||||
self._attr_device_class = CoverDeviceClass.SHUTTER
|
||||
|
||||
@property
|
||||
def supported_features(self) -> CoverEntityFeature:
|
||||
"""Flag supported features."""
|
||||
supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.STOP
|
||||
)
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= (
|
||||
if isinstance(node, Blind):
|
||||
self._attr_device_class = CoverDeviceClass.BLIND
|
||||
self._is_blind = True
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
)
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
|
||||
@@ -59,7 +59,7 @@ from .utils import (
|
||||
get_burners,
|
||||
get_circuits,
|
||||
get_compressors,
|
||||
get_condensors,
|
||||
get_condensers,
|
||||
get_device_serial,
|
||||
get_evaporators,
|
||||
is_supported,
|
||||
@@ -1237,10 +1237,10 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
CONDENSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
CONDENSER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="condensor_liquid_temperature",
|
||||
translation_key="condensor_liquid_temperature",
|
||||
key="condenser_liquid_temperature",
|
||||
translation_key="condenser_liquid_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_getter=lambda api: api.getCondensorLiquidTemperature(),
|
||||
@@ -1248,8 +1248,8 @@ CONDENSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="condensor_subcooling_temperature",
|
||||
translation_key="condensor_subcooling_temperature",
|
||||
key="condenser_subcooling_temperature",
|
||||
translation_key="condenser_subcooling_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_getter=lambda api: api.getCondensorSubcoolingTemperature(),
|
||||
@@ -1303,7 +1303,7 @@ def _build_entities(
|
||||
(get_circuits(device.api), CIRCUIT_SENSORS),
|
||||
(get_burners(device.api), BURNER_SENSORS),
|
||||
(get_compressors(device.api), COMPRESSOR_SENSORS),
|
||||
(get_condensors(device.api), CONDENSOR_SENSORS),
|
||||
(get_condensers(device.api), CONDENSER_SENSORS),
|
||||
(get_evaporators(device.api), EVAPORATOR_SENSORS),
|
||||
):
|
||||
entities.extend(
|
||||
|
||||
@@ -244,11 +244,11 @@
|
||||
"compressor_starts": {
|
||||
"name": "Compressor starts"
|
||||
},
|
||||
"condensor_liquid_temperature": {
|
||||
"name": "Condensor liquid temperature"
|
||||
"condenser_liquid_temperature": {
|
||||
"name": "Condenser liquid temperature"
|
||||
},
|
||||
"condensor_subcooling_temperature": {
|
||||
"name": "Condensor subcooling temperature"
|
||||
"condenser_subcooling_temperature": {
|
||||
"name": "Condenser subcooling temperature"
|
||||
},
|
||||
"dhw_storage_bottom_temperature": {
|
||||
"name": "DHW storage bottom temperature"
|
||||
|
||||
@@ -130,14 +130,14 @@ def get_compressors(device: PyViCareDevice) -> list[PyViCareHeatingDeviceCompone
|
||||
return []
|
||||
|
||||
|
||||
def get_condensors(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]:
|
||||
"""Return the list of condensors."""
|
||||
def get_condensers(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]:
|
||||
"""Return the list of condensers."""
|
||||
try:
|
||||
return device.condensors
|
||||
except PyViCareNotSupportedFeatureError:
|
||||
_LOGGER.debug("No condensors found")
|
||||
_LOGGER.debug("No condensers found")
|
||||
except AttributeError as error:
|
||||
_LOGGER.debug("No condensors found: %s", error)
|
||||
_LOGGER.debug("No condensers found: %s", error)
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,6 @@ class ZWaveBaseEntity(Entity):
|
||||
)
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = self.generate_name()
|
||||
self._attr_unique_id = get_unique_id(driver, self.info.primary_value.value_id)
|
||||
if isinstance(info, NewZwaveDiscoveryInfo):
|
||||
self.entity_description = info.entity_description
|
||||
else:
|
||||
@@ -80,6 +78,8 @@ class ZWaveBaseEntity(Entity):
|
||||
self._attr_entity_registry_enabled_default = enabled_default
|
||||
if (entity_category := info.entity_category) is not None:
|
||||
self._attr_entity_category = entity_category
|
||||
self._attr_name = self.generate_name()
|
||||
self._attr_unique_id = get_unique_id(driver, self.info.primary_value.value_id)
|
||||
self._attr_assumed_state = self.info.assumed_state
|
||||
# device is precreated in main handler
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
||||
@@ -5689,7 +5689,7 @@
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "RuuviTag BLE"
|
||||
"name": "Ruuvi BLE"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ICON,
|
||||
@@ -418,7 +417,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"extra_state_attributes",
|
||||
"force_update",
|
||||
"icon",
|
||||
"included_unique_ids",
|
||||
"name",
|
||||
"should_poll",
|
||||
"state",
|
||||
@@ -526,9 +524,6 @@ class Entity(
|
||||
__capabilities_updated_at_reported: bool = False
|
||||
__remove_future: asyncio.Future[None] | None = None
|
||||
|
||||
# A list of included entity IDs in case the entity represents a group
|
||||
_included_entities: list[str] | None = None
|
||||
|
||||
# Entity Properties
|
||||
_attr_assumed_state: bool = False
|
||||
_attr_attribution: str | None = None
|
||||
@@ -544,7 +539,6 @@ class Entity(
|
||||
_attr_extra_state_attributes: dict[str, Any]
|
||||
_attr_force_update: bool
|
||||
_attr_icon: str | None
|
||||
_attr_included_unique_ids: list[str]
|
||||
_attr_name: str | None
|
||||
_attr_should_poll: bool = True
|
||||
_attr_state: StateType = STATE_UNKNOWN
|
||||
@@ -1091,21 +1085,6 @@ class Entity(
|
||||
available = self.available # only call self.available once per update cycle
|
||||
state = self._stringify_state(available)
|
||||
if available:
|
||||
if self.included_unique_ids is not None:
|
||||
entity_registry = er.async_get(self.hass)
|
||||
self._included_entities = [
|
||||
entity_id
|
||||
for included_id in self.included_unique_ids
|
||||
if (
|
||||
entity_id := entity_registry.async_get_entity_id(
|
||||
self.platform.domain,
|
||||
self.platform.platform_name,
|
||||
included_id,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
]
|
||||
attr[ATTR_ENTITY_ID] = self._included_entities.copy()
|
||||
if state_attributes := self.state_attributes:
|
||||
attr |= state_attributes
|
||||
if extra_state_attributes := self.extra_state_attributes:
|
||||
@@ -1395,30 +1374,6 @@ class Entity(
|
||||
|
||||
async def add_to_platform_finish(self) -> None:
|
||||
"""Finish adding an entity to a platform."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
async def _handle_entity_registry_updated(event: Event[Any]) -> None:
|
||||
"""Handle registry create or update event."""
|
||||
if (
|
||||
event.data["action"] in {"create", "update"}
|
||||
and (entry := entity_registry.async_get(event.data["entity_id"]))
|
||||
and self.included_unique_ids is not None
|
||||
and entry.unique_id in self.included_unique_ids
|
||||
) or (
|
||||
event.data["action"] == "remove"
|
||||
and self._included_entities is not None
|
||||
and event.data["entity_id"] in self._included_entities
|
||||
):
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self.included_unique_ids is not None:
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
_handle_entity_registry_updated,
|
||||
)
|
||||
)
|
||||
|
||||
await self.async_internal_added_to_hass()
|
||||
await self.async_added_to_hass()
|
||||
self._platform_state = EntityPlatformState.ADDED
|
||||
@@ -1678,16 +1633,6 @@ class Entity(
|
||||
self.hass, integration_domain=platform_name, module=type(self).__module__
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def included_unique_ids(self) -> list[str] | None:
|
||||
"""Return the list of unique IDs if the entity represents a group.
|
||||
|
||||
The corresponding entities will be shown as members in the UI.
|
||||
"""
|
||||
if hasattr(self, "_attr_included_unique_ids"):
|
||||
return self._attr_included_unique_ids
|
||||
return None
|
||||
|
||||
|
||||
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""A class that describes toggle entities."""
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -2128,7 +2128,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.1.2
|
||||
pylamarzocco==2.1.3
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1772,7 +1772,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.1.2
|
||||
pylamarzocco==2.1.3
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
|
||||
@@ -32,6 +32,7 @@ FIELD_SCHEMA = vol.Schema(
|
||||
vol.Optional("default"): exists,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional(CONF_SELECTOR): selector.validate_selector,
|
||||
vol.Optional("context"): {str: str},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""The tests for LG NEtcast device triggers."""
|
||||
"""The tests for LG Netcast device triggers."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -19,6 +22,13 @@ from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast
|
||||
from tests.common import MockConfigEntry, async_get_device_automations
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_lg_netcast() -> Generator[None]:
|
||||
"""Mock LG Netcast library."""
|
||||
with patch("homeassistant.components.lg_netcast.LgNetCastClient"):
|
||||
yield
|
||||
|
||||
|
||||
async def test_get_triggers(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The tests for LG Netcast device triggers."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -17,6 +18,13 @@ from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast
|
||||
from tests.common import MockEntity, MockEntityPlatform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_lg_netcast() -> Generator[None]:
|
||||
"""Mock LG Netcast library."""
|
||||
with patch("homeassistant.components.lg_netcast.LgNetCastClient"):
|
||||
yield
|
||||
|
||||
|
||||
async def test_lg_netcast_turn_on_trigger_device_id(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Test package for RuuviTag BLE sensor integration."""
|
||||
"""Test package for Ruuvi BLE sensor integration."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Fixtures for testing RuuviTag BLE."""
|
||||
"""Fixtures for testing Ruuvi BLE."""
|
||||
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test the Ruuvitag BLE sensors."""
|
||||
"""Test the Ruuvi BLE sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -35,7 +35,7 @@ async def test_sensors(
|
||||
snapshot: SnapshotAssertion,
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> None:
|
||||
"""Test the RuuviTag BLE sensors."""
|
||||
"""Test the Ruuvi BLE sensors."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, unique_id=service_info.address)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ from homeassistant.components.application_credentials import (
|
||||
)
|
||||
from homeassistant.components.senz.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import CLIENT_ID, CLIENT_SECRET
|
||||
from .const import CLIENT_ID, CLIENT_SECRET, ENTRY_UNIQUE_ID
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
@@ -63,7 +64,7 @@ def mock_expires_at() -> float:
|
||||
def mock_config_entry(hass: HomeAssistant, expires_at: float) -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
minor_version=1,
|
||||
minor_version=2,
|
||||
domain=DOMAIN,
|
||||
title="Senz test",
|
||||
data={
|
||||
@@ -77,6 +78,7 @@ def mock_config_entry(hass: HomeAssistant, expires_at: float) -> MockConfigEntry
|
||||
},
|
||||
},
|
||||
entry_id="senz_test",
|
||||
unique_id=ENTRY_UNIQUE_ID,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
@@ -109,3 +111,20 @@ async def setup_credentials(hass: HomeAssistant) -> None:
|
||||
),
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def access_token(hass: HomeAssistant) -> str:
|
||||
"""Return a valid access token."""
|
||||
return config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"sub": ENTRY_UNIQUE_ID,
|
||||
"aud": [],
|
||||
"scp": [
|
||||
"rest_api",
|
||||
"offline_access",
|
||||
],
|
||||
"ou_code": "NA",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
|
||||
CLIENT_ID = "test_client_id"
|
||||
CLIENT_SECRET = "test_client_secret"
|
||||
|
||||
ENTRY_UNIQUE_ID = "test_unique_id"
|
||||
|
||||
@@ -12,11 +12,13 @@ from homeassistant.components.application_credentials import (
|
||||
)
|
||||
from homeassistant.components.senz.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import CLIENT_ID, CLIENT_SECRET
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
@@ -26,6 +28,7 @@ async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
"""Check full flow."""
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
@@ -61,7 +64,7 @@ async def test_full_flow(
|
||||
TOKEN_ENDPOINT,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"access_token": access_token,
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
@@ -74,3 +77,52 @@ async def test_full_flow(
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_duplicate_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
"""Check full flow with duplicate entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": "https://example.com/auth/external/callback",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["url"] == (
|
||||
f"{AUTHORIZATION_ENDPOINT}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=restapi+offline_access"
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == 200
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.post(
|
||||
TOKEN_ENDPOINT,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": access_token,
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.senz.async_setup_entry", return_value=True):
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.senz.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -9,6 +10,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from . import setup_integration
|
||||
from .const import ENTRY_UNIQUE_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -43,3 +45,36 @@ async def test_oauth_implementation_not_available(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_migrate_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_senz_client: MagicMock,
|
||||
expires_at: float,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
"""Test migration of config entry."""
|
||||
mock_entry_v1_1 = MockConfigEntry(
|
||||
version=1,
|
||||
minor_version=1,
|
||||
domain=DOMAIN,
|
||||
title="SENZ test",
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": access_token,
|
||||
"scope": "rest_api offline_access",
|
||||
"expires_in": 86399,
|
||||
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
|
||||
"token_type": "Bearer",
|
||||
"expires_at": expires_at,
|
||||
},
|
||||
},
|
||||
entry_id="senz_test",
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_entry_v1_1)
|
||||
assert mock_entry_v1_1.version == 1
|
||||
assert mock_entry_v1_1.minor_version == 2
|
||||
assert mock_entry_v1_1.unique_id == ENTRY_UNIQUE_ID
|
||||
|
||||
@@ -192,7 +192,7 @@ async def _create_device(hass: HomeAssistant, mock_device_code: str) -> Customer
|
||||
|
||||
device.function = {
|
||||
key: DeviceFunction(
|
||||
code=value.get("code"),
|
||||
code=key,
|
||||
type=value["type"],
|
||||
values=json_dumps(value["value"]),
|
||||
)
|
||||
@@ -200,7 +200,7 @@ async def _create_device(hass: HomeAssistant, mock_device_code: str) -> Customer
|
||||
}
|
||||
device.status_range = {
|
||||
key: DeviceStatusRange(
|
||||
code=value.get("code"),
|
||||
code=key,
|
||||
type=value["type"],
|
||||
values=json_dumps(value["value"]),
|
||||
)
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"switch_kb_light": false,
|
||||
"telnet_state": "sim_card_no",
|
||||
"muffling": false,
|
||||
"alarm_msg": "AFMAZQBuAHMAbwByACAATABvAHcAIABCAGEAdAB0AGUAcgB5AAoAWgBvAG4AZQA6ADAAMAA1AEUAbgB0AHIAYQBuAGMAZQ==",
|
||||
"alarm_msg": "**REDACTED**",
|
||||
"switch_alarm_propel": true,
|
||||
"alarm_delay_time": 20,
|
||||
"master_state": "normal",
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
"wireless_lowpower": 10,
|
||||
"wireless_awake": false,
|
||||
"pir_switch": 3,
|
||||
"doorbell_pic": "",
|
||||
"doorbell_pic": "**REDACTED**",
|
||||
"basic_device_volume": 51,
|
||||
"humanoid_filter": false,
|
||||
"alarm_message": "**REDACTED**",
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
"sd_format_state": 0,
|
||||
"motion_switch": false,
|
||||
"doorbell_active": "",
|
||||
"doorbell_pic": "aHR0cHM6Ly90eS1ldS1zdG9yYWdlMzAtcGljLnMzLmV1LWNlbnRyYWwtMS5hbWF6b25hd3MuY29tL2U0ODYwMy0yMjU2NjYxOC1zempzYjU0ZDE2ZGI0ZTQ3OTAxYS9kZXRlY3QvMTc2MjE5OTIyMS5qcGVnP1gtQW16LVNlY3VyaXR5LVRva2VuPUZ3b0daWEl2WVhkekVLMyUyRiUyRiUyRiUyRiUyRiUyRiUyRiUyRiUyRiUyRndFYURDUmJiZDNWWldORmtsWUliQ0tDQXZCZCUyQnEwY2EzRURkZzdONTJqUDhmWUI3WVNSS0huSDNnRXZDRjh6OHpMSU92bkZrdG1UQWFLVldSNkxsMDlMMTJ6b09wR2ptekwwRGIyR1NRSG1uSmJNZXRhSm9nWlRQeGI4eGdMbTRwVkhidTkyZndib29UVVllMUwycmhNJTJCdiUyQkFtVG9DTVdwWE9sNThXUDVwZDAwSmdIWGlBUzVGWnhndVR5UWNJcmxFeG5JeW4wYzgwa0VRMjlVa3d2VThMRVpDeUtwTFlIRjJlYTElMkYlMkZPaUk2b1hrdVF3TU0lMkZCWHMlMkJYMWVYYWdnJTJGaW1oRUVhJTJCQ1REODUlMkYlMkZlSHVqZm1KRSUyQnIyeERkdmgwSUJPTFMwYWc1Zm9EbyUyRjZpRHpXMHNKZE1tTjdPNVhiMnMwRnM4MUxwWG5wTXdKRFRxbUklMkJFSDVyYzlxT0NHemY1SUZqbnZZMGF3TjY1blVsMWlpeWphVElCaklwZWVva2htU1F6WlBVJTJGdERzRHlGYUJRRXFWNjkyemlGdVluWHozdnlqdHlzOU5JWG1aJTJGd1hRaTglM0QmWC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BU0lBVVRQTVVKSkpRTlVFUEozSCUyRjIwMjUxMTAzJTJGZXUtY2VudHJhbC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTEwM1QxOTQ3MDRaJlgtQW16LUV4cGlyZXM9NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmJ1Y2tldD10eS1ldS1zdG9yYWdlMzAtcGljJnY9MS4wJlgtQW16LVNpZ25hdHVyZT05YTFlZTYyNWVlMGM5NmQ5NzViMjg2OGQxOGNlOTA3YzU0YTExNjgyNGMzZjkwYzI3YTlmNTNjYjNhN2E0MjA0",
|
||||
"doorbell_pic": "**REDACTED**",
|
||||
"device_restart": false,
|
||||
"alarm_message": "eyJ2IjoiNS4wIiwiZmlsZXMiOlt7ImRhdGEiOiJhMThiNDM0YmJmZDY1NGM3N2UzNTc2MWRlMDgyZTc2OGZjM2JmYmQ2NThlZDAyMGIwZGJhZjQ2OTE1YTEwY2NjZDI5YjUxZTY1YjBkNjJiMzAxNmVlZDU0YjU1MTU1ZjE1NzkwNTk2ZDc2YzgwYWFlOWU3ODQ0N2QwYzFlOWNmNmIzMWRlN2ZiOWQyOWU4ZWEwODhlYzAxOGJhYTRhNWMzZjBlMDFmYThiOTRiNGQzYWVkNDk4ZGIwOTUyOTc1ZWQ5ODY2OTNlNmM1NDMyYWY3YTE5N2FiYTA3ZWE3YjJkZGNmZDRjMzQ2N2Q5ZDAwMmJkMDc4OWQ0OTYzNWI1NzkyIiwia2V5SWQiOiJkZWZhdWx0IiwiaXYiOiJjN2JiMTk2Mjc1MWRmOThhZWRiM2VjMGU3Mjk4MWVmMCJ9XSwiY21kIjoiaXBjX2Rvb3JiZWxsIiwidHlwZSI6ImltYWdlIn0="
|
||||
"alarm_message": "**REDACTED**"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
"record_switch": true,
|
||||
"record_mode": 1,
|
||||
"pir_switch": 2,
|
||||
"doorbell_pic": "",
|
||||
"doorbell_pic": "**REDACTED**",
|
||||
"siren_switch": false,
|
||||
"basic_device_volume": 1,
|
||||
"motion_tracking": true,
|
||||
|
||||
@@ -1,4 +1,305 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_diagnostics[mal_gyitctrjj1kefxp2]
|
||||
dict({
|
||||
'active_time': '2024-12-02T20:08:56+00:00',
|
||||
'category': 'mal',
|
||||
'create_time': '2024-12-02T20:08:56+00:00',
|
||||
'disabled_by': None,
|
||||
'disabled_polling': False,
|
||||
'endpoint': 'https://apigw.tuyaeu.com',
|
||||
'function': dict({
|
||||
'alarm_delay_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"min","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'delay_set': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'master_mode': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["disarmed","arm","home","sos"]}',
|
||||
}),
|
||||
'master_state': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["normal","alarm"]}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'sub_admin': dict({
|
||||
'type': 'Raw',
|
||||
'value': '{}',
|
||||
}),
|
||||
'sub_class': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["remote_controller","detector"]}',
|
||||
}),
|
||||
'switch_alarm_light': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_alarm_propel': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_alarm_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_kb_light': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_kb_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_mode_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
}),
|
||||
'home_assistant': dict({
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entities': list([
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': None,
|
||||
'icon': None,
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'changed_by': None,
|
||||
'code_arm_required': False,
|
||||
'code_format': None,
|
||||
'friendly_name': 'Multifunction alarm',
|
||||
'supported_features': 11,
|
||||
}),
|
||||
'entity_id': 'alarm_control_panel.multifunction_alarm',
|
||||
'state': 'disarmed',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': 'config',
|
||||
'icon': None,
|
||||
'original_device_class': 'duration',
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Multifunction alarm Arm delay',
|
||||
'max': 999.0,
|
||||
'min': 0.0,
|
||||
'mode': 'auto',
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
'entity_id': 'number.multifunction_alarm_arm_delay',
|
||||
'state': '15.0',
|
||||
}),
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': 'config',
|
||||
'icon': None,
|
||||
'original_device_class': 'duration',
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Multifunction alarm Alarm delay',
|
||||
'max': 999.0,
|
||||
'min': 0.0,
|
||||
'mode': 'auto',
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
'entity_id': 'number.multifunction_alarm_alarm_delay',
|
||||
'state': '20.0',
|
||||
}),
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': 'config',
|
||||
'icon': None,
|
||||
'original_device_class': 'duration',
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Multifunction alarm Siren duration',
|
||||
'max': 999.0,
|
||||
'min': 0.0,
|
||||
'mode': 'auto',
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': 'min',
|
||||
}),
|
||||
'entity_id': 'number.multifunction_alarm_siren_duration',
|
||||
'state': '3.0',
|
||||
}),
|
||||
'unit_of_measurement': 'min',
|
||||
}),
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': 'config',
|
||||
'icon': None,
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'friendly_name': 'Multifunction alarm Arm beep',
|
||||
}),
|
||||
'entity_id': 'switch.multifunction_alarm_arm_beep',
|
||||
'state': 'on',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'device_class': None,
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'entity_category': 'config',
|
||||
'icon': None,
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'friendly_name': 'Multifunction alarm Siren',
|
||||
}),
|
||||
'entity_id': 'switch.multifunction_alarm_siren',
|
||||
'state': 'on',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
]),
|
||||
'name': 'Multifunction alarm',
|
||||
'name_by_user': None,
|
||||
}),
|
||||
'id': '2pxfek1jjrtctiyglam',
|
||||
'mqtt_connected': True,
|
||||
'name': 'Multifunction alarm',
|
||||
'online': True,
|
||||
'product_id': 'gyitctrjj1kefxp2',
|
||||
'product_name': 'Multifunction alarm',
|
||||
'set_up': True,
|
||||
'status': dict({
|
||||
'alarm_delay_time': 20,
|
||||
'alarm_msg': '**REDACTED**',
|
||||
'alarm_time': 3,
|
||||
'delay_set': 15,
|
||||
'master_mode': 'disarmed',
|
||||
'master_state': 'normal',
|
||||
'muffling': False,
|
||||
'sub_admin': 'AgEFCggC////HABLAGkAdABjAGgAZQBuACAAUwBtAG8AawBlACBjAAL///8gAHUAbgBkAGUAbABlAHQAYQBiAGwAZQA6AEUATwBMADFkAAL///8gAHUAbgBkAGUAbABlAHQAYQBiAGwAZQA6AEUATwBMADJlAAL///8gAHUAbgBkAGUAbABlAHQAYQBiAGwAZQA6AEUATwBMADNmAAL///8gAHUAbgBkAGUAbABlAHQAYQBiAGwAZQA6AEUATwBMADQ=',
|
||||
'sub_class': 'remote_controller',
|
||||
'sub_state': 'normal',
|
||||
'switch_alarm_light': True,
|
||||
'switch_alarm_propel': True,
|
||||
'switch_alarm_sound': True,
|
||||
'switch_kb_light': False,
|
||||
'switch_kb_sound': False,
|
||||
'switch_mode_sound': True,
|
||||
'telnet_state': 'sim_card_no',
|
||||
}),
|
||||
'status_range': dict({
|
||||
'alarm_delay_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'alarm_msg': dict({
|
||||
'type': 'Raw',
|
||||
'value': '{}',
|
||||
}),
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"min","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'delay_set': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'master_mode': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["disarmed","arm","home","sos"]}',
|
||||
}),
|
||||
'master_state': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["normal","alarm"]}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'sub_admin': dict({
|
||||
'type': 'Raw',
|
||||
'value': '{}',
|
||||
}),
|
||||
'sub_class': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["remote_controller","detector"]}',
|
||||
}),
|
||||
'sub_state': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["normal","alarm","fault","others"]}',
|
||||
}),
|
||||
'switch_alarm_light': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_alarm_propel': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_alarm_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_kb_light': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_kb_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'switch_mode_sound': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'telnet_state': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["normal","network_no","phone_no","sim_card_no","network_search","signal_level_1","signal_level_2","signal_level_3","signal_level_4","signal_level_5"]}',
|
||||
}),
|
||||
}),
|
||||
'sub': False,
|
||||
'support_local': True,
|
||||
'terminal_id': '7cd96aff-6ec8-4006-b093-3dbff7947591',
|
||||
'time_zone': '+02:00',
|
||||
'update_time': '2024-12-02T20:08:56+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_device_diagnostics[rqbj_4iqe2hsfyd86kwwc]
|
||||
dict({
|
||||
'active_time': '2025-06-24T20:33:10+00:00',
|
||||
@@ -8,7 +309,15 @@
|
||||
'disabled_polling': False,
|
||||
'endpoint': 'https://apigw.tuyaeu.com',
|
||||
'function': dict({
|
||||
'null': dict({
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":3600,"scale":0,"step":1}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'self_checking': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
@@ -74,7 +383,27 @@
|
||||
'self_checking': False,
|
||||
}),
|
||||
'status_range': dict({
|
||||
'null': dict({
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":3600,"scale":0,"step":1}',
|
||||
}),
|
||||
'checking_result': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["checking","check_success","check_failure","others"]}',
|
||||
}),
|
||||
'gas_sensor_status': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["alarm","normal"]}',
|
||||
}),
|
||||
'gas_sensor_value': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"ppm","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'self_checking': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
@@ -94,7 +423,15 @@
|
||||
'category': 'rqbj',
|
||||
'create_time': '2025-06-24T20:33:10+00:00',
|
||||
'function': dict({
|
||||
'null': dict({
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":3600,"scale":0,"step":1}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'self_checking': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
@@ -159,7 +496,27 @@
|
||||
'self_checking': False,
|
||||
}),
|
||||
'status_range': dict({
|
||||
'null': dict({
|
||||
'alarm_time': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"s","min":0,"max":3600,"scale":0,"step":1}',
|
||||
}),
|
||||
'checking_result': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["checking","check_success","check_failure","others"]}',
|
||||
}),
|
||||
'gas_sensor_status': dict({
|
||||
'type': 'Enum',
|
||||
'value': '{"range":["alarm","normal"]}',
|
||||
}),
|
||||
'gas_sensor_value': dict({
|
||||
'type': 'Integer',
|
||||
'value': '{"unit":"ppm","min":0,"max":999,"scale":0,"step":1}',
|
||||
}),
|
||||
'muffling': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
'self_checking': dict({
|
||||
'type': 'Boolean',
|
||||
'value': '{}',
|
||||
}),
|
||||
|
||||
@@ -80,54 +80,63 @@ async def test_service(
|
||||
mock_manager.send_commands.assert_called_once_with(mock_device.id, [command])
|
||||
|
||||
|
||||
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.ALARM_CONTROL_PANEL])
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device_code",
|
||||
["mal_gyitctrjj1kefxp2"],
|
||||
)
|
||||
async def test_alarm_state_triggered(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device: CustomerDevice,
|
||||
) -> None:
|
||||
"""Test alarm state returns TRIGGERED for non-battery alarms."""
|
||||
entity_id = "alarm_control_panel.multifunction_alarm"
|
||||
|
||||
# Set up alarm state without battery warning
|
||||
mock_device.status["master_state"] = "alarm"
|
||||
mock_device.status["alarm_msg"] = (
|
||||
"AFQAZQBzAHQAIABTAGUAbgBzAG8Acg==" # "Test Sensor" in UTF-16BE
|
||||
)
|
||||
|
||||
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None, f"{entity_id} does not exist"
|
||||
assert state.state == AlarmControlPanelState.TRIGGERED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device_code",
|
||||
["mal_gyitctrjj1kefxp2"],
|
||||
("status_updates", "expected_state"),
|
||||
[
|
||||
(
|
||||
{"master_mode": "disarmed"},
|
||||
AlarmControlPanelState.DISARMED,
|
||||
),
|
||||
(
|
||||
{"master_mode": "arm"},
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
),
|
||||
(
|
||||
{"master_mode": "home"},
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
),
|
||||
(
|
||||
{"master_mode": "sos"},
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
),
|
||||
(
|
||||
{
|
||||
"master_mode": "home",
|
||||
"master_state": "alarm",
|
||||
# "Test Sensor" in UTF-16BE
|
||||
"alarm_msg": "AFQAZQBzAHQAIABTAGUAbgBzAG8Acg==",
|
||||
},
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
),
|
||||
(
|
||||
{
|
||||
"master_mode": "home",
|
||||
"master_state": "alarm",
|
||||
# "Sensor Low Battery Test Sensor" in UTF-16BE
|
||||
"alarm_msg": "AFMAZQBuAHMAbwByACAATABvAHcAIABCAGEAdAB0AGUAcgB5ACAAVABlAHMAdAAgAFMAZQBuAHMAbwBy",
|
||||
},
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_alarm_state_battery_warning(
|
||||
async def test_state(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device: CustomerDevice,
|
||||
status_updates: dict[str, Any],
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test alarm state ignores battery warnings."""
|
||||
"""Test state."""
|
||||
entity_id = "alarm_control_panel.multifunction_alarm"
|
||||
|
||||
# Set up alarm state with battery warning
|
||||
mock_device.status["master_state"] = "alarm"
|
||||
mock_device.status["alarm_msg"] = (
|
||||
"AFMAZQBuAHMAbwByACAATABvAHcAIABCAGEAdAB0AGUAcgB5ACAAVABlAHMAdAAgAFMAZQBuAHMAbwBy" # "Sensor Low Battery Test Sensor" in UTF-16BE
|
||||
)
|
||||
|
||||
mock_device.status.update(status_updates)
|
||||
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None, f"{entity_id} does not exist"
|
||||
# Should not be triggered for battery warnings
|
||||
assert state.state != AlarmControlPanelState.TRIGGERED
|
||||
assert state.state == expected_state
|
||||
|
||||
@@ -42,7 +42,13 @@ async def test_entry_diagnostics(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"])
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device_code",
|
||||
[
|
||||
"mal_gyitctrjj1kefxp2",
|
||||
"rqbj_4iqe2hsfyd86kwwc",
|
||||
],
|
||||
)
|
||||
async def test_device_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
|
||||
@@ -6,6 +6,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.tuya.const import DOMAIN
|
||||
from homeassistant.components.tuya.diagnostics import _REDACTED_DPCODES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
@@ -65,3 +66,11 @@ async def test_fixtures_valid(hass: HomeAssistant) -> None:
|
||||
assert key not in details, (
|
||||
f"Please remove data[`'{key}']` from {device_code}.json"
|
||||
)
|
||||
if "status" in details:
|
||||
statuses = details["status"]
|
||||
for key in statuses:
|
||||
if key in _REDACTED_DPCODES:
|
||||
assert statuses[key] == "**REDACTED**", (
|
||||
f"Please mark `data['status']['{key}']` as `**REDACTED**`"
|
||||
f" in {device_code}.json"
|
||||
)
|
||||
|
||||
52
tests/components/velux/snapshots/test_cover.ambr
Normal file
52
tests/components/velux/snapshots/test_cover.ambr
Normal file
@@ -0,0 +1,52 @@
|
||||
# serializer version: 1
|
||||
# name: test_cover_setup[cover.test_window-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.test_window',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.WINDOW: 'window'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'velux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_setup[cover.test_window-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 70,
|
||||
'device_class': 'window',
|
||||
'friendly_name': 'Test Window',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.test_window',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
||||
@@ -1,32 +1,69 @@
|
||||
"""Tests for the Velux cover platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.velux import DOMAIN
|
||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import update_callback_entity
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pyvlx")
|
||||
@pytest.fixture
|
||||
def platform() -> Platform:
|
||||
"""Fixture to specify platform to test."""
|
||||
return Platform.COVER
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_integration")
|
||||
async def test_cover_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Snapshot the cover entity (registry + state)."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
|
||||
# Get the cover entity setup and test device association
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entity_entries) == 1
|
||||
entry = entity_entries[0]
|
||||
|
||||
assert entry.device_id is not None
|
||||
device_entry = device_registry.async_get(entry.device_id)
|
||||
assert device_entry is not None
|
||||
assert (DOMAIN, f"{123456789}") in device_entry.identifiers
|
||||
assert device_entry.via_device_id is not None
|
||||
via_device_entry = device_registry.async_get(device_entry.via_device_id)
|
||||
assert via_device_entry is not None
|
||||
assert (
|
||||
DOMAIN,
|
||||
f"gateway_{mock_config_entry.entry_id}",
|
||||
) in via_device_entry.identifiers
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_integration")
|
||||
async def test_cover_closed(
|
||||
hass: HomeAssistant,
|
||||
mock_window: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the cover closed state."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.velux.PLATFORMS", [Platform.COVER]):
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
test_entity_id = "cover.test_window"
|
||||
|
||||
# Initial state should be open
|
||||
|
||||
@@ -3283,7 +3283,7 @@
|
||||
'state': '5067',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.model2_condensor_subcooling_temperature-entry]
|
||||
# name: test_all_entities[sensor.model2_condenser_subcooling_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3296,7 +3296,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.model2_condensor_subcooling_temperature',
|
||||
'entity_id': 'sensor.model2_condenser_subcooling_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -3311,25 +3311,25 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Condensor subcooling temperature',
|
||||
'original_name': 'Condenser subcooling temperature',
|
||||
'platform': 'vicare',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'condensor_subcooling_temperature',
|
||||
'unique_id': 'gateway2_################-condensor_subcooling_temperature-0',
|
||||
'translation_key': 'condenser_subcooling_temperature',
|
||||
'unique_id': 'gateway2_################-condenser_subcooling_temperature-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.model2_condensor_subcooling_temperature-state]
|
||||
# name: test_all_entities[sensor.model2_condenser_subcooling_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'model2 Condensor subcooling temperature',
|
||||
'friendly_name': 'model2 Condenser subcooling temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.model2_condensor_subcooling_temperature',
|
||||
'entity_id': 'sensor.model2_condenser_subcooling_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -6,7 +6,7 @@ import dataclasses
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any, final
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@@ -20,7 +20,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
@@ -1879,7 +1878,6 @@ async def test_change_entity_id(
|
||||
self.remove_calls = []
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self.added_calls.append(None)
|
||||
self.async_on_remove(lambda: result.append(1))
|
||||
|
||||
@@ -2898,103 +2896,3 @@ async def test_platform_state_write_from_init_unique_id(
|
||||
# The early attempt to write is interpreted as a unique ID collision
|
||||
assert "Platform test_platform does not generate unique IDs." in caplog.text
|
||||
assert "Entity id already exists - ignoring: test.test" not in caplog.text
|
||||
|
||||
|
||||
async def test_included_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test included entities are exposed via the entity_id attribute."""
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
domain="hello",
|
||||
platform="test",
|
||||
unique_id="very_unique_oceans",
|
||||
suggested_object_id="oceans",
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
domain="hello",
|
||||
platform="test",
|
||||
unique_id="very_unique_continents",
|
||||
suggested_object_id="continents",
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
domain="hello",
|
||||
platform="test",
|
||||
unique_id="very_unique_moon",
|
||||
suggested_object_id="moon",
|
||||
)
|
||||
|
||||
class MockHelloBaseClass(entity.Entity):
|
||||
"""Domain base entity platform domain Hello."""
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {"extra": "beer"}
|
||||
|
||||
class MockHelloIncludedEntitiesClass(MockHelloBaseClass, entity.Entity):
|
||||
"""Mock hello grouped entity class for a test integration."""
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="hello", platform_name="test")
|
||||
mock_entity = MockHelloIncludedEntitiesClass()
|
||||
mock_entity.hass = hass
|
||||
mock_entity.entity_id = "hello.universe"
|
||||
mock_entity.unique_id = "very_unique_universe"
|
||||
mock_entity._attr_included_unique_ids = [
|
||||
"very_unique_continents",
|
||||
"very_unique_oceans",
|
||||
]
|
||||
|
||||
await platform.async_add_entities([mock_entity])
|
||||
|
||||
# Initiate mock grouped entity for hello domain
|
||||
mock_entity.async_schedule_update_ha_state(True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(mock_entity.entity_id)
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.continents", "hello.oceans"]
|
||||
|
||||
# Add an entity to the group of included entities
|
||||
mock_entity._attr_included_unique_ids = [
|
||||
"very_unique_continents",
|
||||
"very_unique_moon",
|
||||
"very_unique_oceans",
|
||||
]
|
||||
mock_entity.async_write_ha_state()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(mock_entity.entity_id)
|
||||
assert state.attributes.get("extra") == "beer"
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == [
|
||||
"hello.continents",
|
||||
"hello.moon",
|
||||
"hello.oceans",
|
||||
]
|
||||
|
||||
# Remove an entity from the group of included entities
|
||||
mock_entity._attr_included_unique_ids = ["very_unique_moon", "very_unique_oceans"]
|
||||
|
||||
mock_entity.async_write_ha_state()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(mock_entity.entity_id)
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon", "hello.oceans"]
|
||||
|
||||
# Rename an included entity via the registry entity
|
||||
entity_registry.async_update_entity(
|
||||
entity_id="hello.moon", new_entity_id="hello.moon_light"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(mock_entity.entity_id)
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light", "hello.oceans"]
|
||||
|
||||
# Remove an included entity from the registry entity
|
||||
entity_registry.async_remove(entity_id="hello.oceans")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(mock_entity.entity_id)
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light"]
|
||||
|
||||
Reference in New Issue
Block a user