forked from home-assistant/core
Compare commits
19 Commits
2023.4.0b1
...
2023.4.0b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 03f085d7be | |||
| b3348c3e6f | |||
| 590db0fa74 | |||
| f56ccf90d9 | |||
| c63f8e714e | |||
| a20771f571 | |||
| 2d482f1f57 | |||
| 499962f4ee | |||
| 88a407361c | |||
| 89dc6db5a7 | |||
| de9e7e47fe | |||
| ab66664f20 | |||
| e7e2532c68 | |||
| 4bf10c01f0 | |||
| aad1f4b766 | |||
| e32d89215d | |||
| 9478518937 | |||
| 8a99d2a566 | |||
| 38aff23be5 |
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20230329.0"]
|
||||
"requirements": ["home-assistant-frontend==20230331.0"]
|
||||
}
|
||||
|
||||
@@ -36,28 +36,28 @@ class LaMetricButtonEntityDescription(
|
||||
BUTTONS = [
|
||||
LaMetricButtonEntityDescription(
|
||||
key="app_next",
|
||||
name="Next app",
|
||||
translation_key="app_next",
|
||||
icon="mdi:arrow-right-bold",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda api: api.app_next(),
|
||||
),
|
||||
LaMetricButtonEntityDescription(
|
||||
key="app_previous",
|
||||
name="Previous app",
|
||||
translation_key="app_previous",
|
||||
icon="mdi:arrow-left-bold",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda api: api.app_previous(),
|
||||
),
|
||||
LaMetricButtonEntityDescription(
|
||||
key="dismiss_current",
|
||||
name="Dismiss current notification",
|
||||
translation_key="dismiss_current",
|
||||
icon="mdi:bell-cancel",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda api: api.dismiss_current_notification(),
|
||||
),
|
||||
LaMetricButtonEntityDescription(
|
||||
key="dismiss_all",
|
||||
name="Dismiss all notifications",
|
||||
translation_key="dismiss_all",
|
||||
icon="mdi:bell-cancel",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda api: api.dismiss_all_notifications(),
|
||||
|
||||
@@ -37,11 +37,10 @@ class LaMetricSelectEntityDescription(
|
||||
SELECTS = [
|
||||
LaMetricSelectEntityDescription(
|
||||
key="brightness_mode",
|
||||
name="Brightness mode",
|
||||
translation_key="brightness_mode",
|
||||
icon="mdi:brightness-auto",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=["auto", "manual"],
|
||||
translation_key="brightness_mode",
|
||||
current_fn=lambda device: device.display.brightness_mode.value,
|
||||
select_fn=lambda api, opt: api.display(brightness_mode=BrightnessMode(opt)),
|
||||
),
|
||||
|
||||
@@ -38,6 +38,7 @@ class LaMetricSensorEntityDescription(
|
||||
SENSORS = [
|
||||
LaMetricSensorEntityDescription(
|
||||
key="rssi",
|
||||
translation_key="rssi",
|
||||
name="Wi-Fi signal",
|
||||
icon="mdi:wifi",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
|
||||
@@ -45,13 +45,38 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"app_next": {
|
||||
"name": "Next app"
|
||||
},
|
||||
"app_previous": {
|
||||
"name": "Previous app"
|
||||
},
|
||||
"dismiss_current": {
|
||||
"name": "Dismiss current notification"
|
||||
},
|
||||
"dismiss_all": {
|
||||
"name": "Dismiss all notifications"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"rssi": {
|
||||
"name": "Wi-Fi signal"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"brightness_mode": {
|
||||
"name": "Brightness mode",
|
||||
"state": {
|
||||
"auto": "Automatic",
|
||||
"manual": "Manual"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class LaMetricSwitchEntityDescription(
|
||||
SWITCHES = [
|
||||
LaMetricSwitchEntityDescription(
|
||||
key="bluetooth",
|
||||
name="Bluetooth",
|
||||
translation_key="bluetooth",
|
||||
icon="mdi:bluetooth",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
available_fn=lambda device: device.bluetooth.available,
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
@@ -285,56 +286,34 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if not self._async_validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_DISARMED)
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_HOME
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_HOME)
|
||||
self._async_update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_AWAY
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_AWAY)
|
||||
self._async_update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_NIGHT
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_NIGHT)
|
||||
self._async_update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
|
||||
"""Send arm vacation command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_VACATION
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_VACATION)
|
||||
self._async_update_state(STATE_ALARM_ARMED_VACATION)
|
||||
|
||||
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
|
||||
"""Send arm custom bypass command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
self._async_update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
@@ -383,18 +362,22 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
|
||||
|
||||
def _async_validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
if self._code is None:
|
||||
return True
|
||||
if (
|
||||
state != STATE_ALARM_DISARMED and not self.code_arm_required
|
||||
) or self._code is None:
|
||||
return
|
||||
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.async_render(
|
||||
parse_result=False, from_state=self._state, to_state=state
|
||||
)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
if not alarm_code or code == alarm_code:
|
||||
return
|
||||
|
||||
raise HomeAssistantError("Invalid alarm code provided")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
@@ -345,56 +346,34 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if not self._async_validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_DISARMED)
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_HOME
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_HOME)
|
||||
self._async_update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_AWAY
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_AWAY)
|
||||
self._async_update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_NIGHT
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_NIGHT)
|
||||
self._async_update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
|
||||
"""Send arm vacation command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_VACATION
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_VACATION)
|
||||
self._async_update_state(STATE_ALARM_ARMED_VACATION)
|
||||
|
||||
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
|
||||
"""Send arm custom bypass command."""
|
||||
if self.code_arm_required and not self._async_validate_code(
|
||||
code, STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
):
|
||||
return
|
||||
|
||||
self._async_validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
self._async_update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
@@ -436,18 +415,22 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
|
||||
|
||||
def _async_validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
if self._code is None:
|
||||
return True
|
||||
if (
|
||||
state != STATE_ALARM_DISARMED and not self.code_arm_required
|
||||
) or self._code is None:
|
||||
return
|
||||
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.async_render(
|
||||
from_state=self._state, to_state=state, parse_result=False
|
||||
)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
if not alarm_code or code == alarm_code:
|
||||
return
|
||||
|
||||
raise HomeAssistantError("Invalid alarm code provided")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
||||
@@ -1355,10 +1355,16 @@ def _context_id_to_bytes(context_id: str | None) -> bytes | None:
|
||||
"""Convert a context_id to bytes."""
|
||||
if context_id is None:
|
||||
return None
|
||||
if len(context_id) == 32:
|
||||
return UUID(context_id).bytes
|
||||
if len(context_id) == 26:
|
||||
return ulid_to_bytes(context_id)
|
||||
with contextlib.suppress(ValueError):
|
||||
# There may be garbage in the context_id column
|
||||
# from custom integrations that are not UUIDs or
|
||||
# ULIDs that filled the column to the max length
|
||||
# so we need to catch the ValueError and return
|
||||
# None if it happens
|
||||
if len(context_id) == 32:
|
||||
return UUID(context_id).bytes
|
||||
if len(context_id) == 26:
|
||||
return ulid_to_bytes(context_id)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1439,12 +1445,15 @@ def migrate_event_type_ids(instance: Recorder) -> bool:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
if events := session.execute(find_event_type_to_migrate()).all():
|
||||
event_types = {event_type for _, event_type in events}
|
||||
if None in event_types:
|
||||
# event_type should never be None but we need to be defensive
|
||||
# so we don't fail the migration because of a bad state
|
||||
event_types.remove(None)
|
||||
event_types.add(_EMPTY_EVENT_TYPE)
|
||||
|
||||
event_type_to_id = event_type_manager.get_many(event_types, session)
|
||||
if missing_event_types := {
|
||||
# We should never see see None for the event_Type in the events table
|
||||
# but we need to be defensive so we don't fail the migration
|
||||
# because of a bad event
|
||||
_EMPTY_EVENT_TYPE if event_type is None else event_type
|
||||
event_type
|
||||
for event_type, event_id in event_type_to_id.items()
|
||||
if event_id is None
|
||||
}:
|
||||
@@ -1470,7 +1479,9 @@ def migrate_event_type_ids(instance: Recorder) -> bool:
|
||||
{
|
||||
"event_id": event_id,
|
||||
"event_type": None,
|
||||
"event_type_id": event_type_to_id[event_type],
|
||||
"event_type_id": event_type_to_id[
|
||||
_EMPTY_EVENT_TYPE if event_type is None else event_type
|
||||
],
|
||||
}
|
||||
for event_id, event_type in events
|
||||
],
|
||||
@@ -1502,14 +1513,17 @@ def migrate_entity_ids(instance: Recorder) -> bool:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
if states := session.execute(find_entity_ids_to_migrate()).all():
|
||||
entity_ids = {entity_id for _, entity_id in states}
|
||||
if None in entity_ids:
|
||||
# entity_id should never be None but we need to be defensive
|
||||
# so we don't fail the migration because of a bad state
|
||||
entity_ids.remove(None)
|
||||
entity_ids.add(_EMPTY_ENTITY_ID)
|
||||
|
||||
entity_id_to_metadata_id = states_meta_manager.get_many(
|
||||
entity_ids, session, True
|
||||
)
|
||||
if missing_entity_ids := {
|
||||
# We should never see _EMPTY_ENTITY_ID in the states table
|
||||
# but we need to be defensive so we don't fail the migration
|
||||
# because of a bad state
|
||||
_EMPTY_ENTITY_ID if entity_id is None else entity_id
|
||||
entity_id
|
||||
for entity_id, metadata_id in entity_id_to_metadata_id.items()
|
||||
if metadata_id is None
|
||||
}:
|
||||
@@ -1537,7 +1551,9 @@ def migrate_entity_ids(instance: Recorder) -> bool:
|
||||
# the history queries still need to work while the
|
||||
# migration is in progress and we will do this in
|
||||
# post_migrate_entity_ids
|
||||
"metadata_id": entity_id_to_metadata_id[entity_id],
|
||||
"metadata_id": entity_id_to_metadata_id[
|
||||
_EMPTY_ENTITY_ID if entity_id is None else entity_id
|
||||
],
|
||||
}
|
||||
for state_id, entity_id in states
|
||||
],
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.5.8"]
|
||||
"requirements": ["reolink-aio==0.5.9"]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ RESOURCE_SETUP = {
|
||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): TextSelector(),
|
||||
}
|
||||
|
||||
NONE_SENTINEL = "none"
|
||||
|
||||
SENSOR_SETUP = {
|
||||
vol.Required(CONF_SELECT): TextSelector(),
|
||||
vol.Optional(CONF_INDEX, default=0): NumberSelector(
|
||||
@@ -102,28 +104,45 @@ SENSOR_SETUP = {
|
||||
),
|
||||
vol.Optional(CONF_ATTRIBUTE): TextSelector(),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(),
|
||||
vol.Optional(CONF_DEVICE_CLASS): SelectSelector(
|
||||
vol.Required(CONF_DEVICE_CLASS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[cls.value for cls in SensorDeviceClass],
|
||||
options=[NONE_SENTINEL]
|
||||
+ sorted(
|
||||
[
|
||||
cls.value
|
||||
for cls in SensorDeviceClass
|
||||
if cls != SensorDeviceClass.ENUM
|
||||
]
|
||||
),
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="device_class",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): SelectSelector(
|
||||
vol.Required(CONF_STATE_CLASS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[cls.value for cls in SensorStateClass],
|
||||
options=[NONE_SENTINEL] + sorted([cls.value for cls in SensorStateClass]),
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="state_class",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): SelectSelector(
|
||||
vol.Required(CONF_UNIT_OF_MEASUREMENT): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[cls.value for cls in UnitOfTemperature],
|
||||
options=[NONE_SENTINEL] + sorted([cls.value for cls in UnitOfTemperature]),
|
||||
custom_value=True,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="unit_of_measurement",
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _strip_sentinel(options: dict[str, Any]) -> None:
|
||||
"""Convert sentinel to None."""
|
||||
for key in (CONF_DEVICE_CLASS, CONF_STATE_CLASS, CONF_UNIT_OF_MEASUREMENT):
|
||||
if options[key] == NONE_SENTINEL:
|
||||
options.pop(key)
|
||||
|
||||
|
||||
async def validate_rest_setup(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
@@ -150,6 +169,7 @@ async def validate_sensor_setup(
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a sub-item so we update the options directly.
|
||||
sensors: list[dict[str, Any]] = handler.options.setdefault(SENSOR_DOMAIN, [])
|
||||
_strip_sentinel(user_input)
|
||||
sensors.append(user_input)
|
||||
return {}
|
||||
|
||||
@@ -181,7 +201,11 @@ async def get_edit_sensor_suggested_values(
|
||||
) -> dict[str, Any]:
|
||||
"""Return suggested values for sensor editing."""
|
||||
idx: int = handler.flow_state["_idx"]
|
||||
return cast(dict[str, Any], handler.options[SENSOR_DOMAIN][idx])
|
||||
suggested_values: dict[str, Any] = dict(handler.options[SENSOR_DOMAIN][idx])
|
||||
for key in (CONF_DEVICE_CLASS, CONF_STATE_CLASS, CONF_UNIT_OF_MEASUREMENT):
|
||||
if not suggested_values.get(key):
|
||||
suggested_values[key] = NONE_SENTINEL
|
||||
return suggested_values
|
||||
|
||||
|
||||
async def validate_sensor_edit(
|
||||
@@ -194,6 +218,7 @@ async def validate_sensor_edit(
|
||||
# In this case, we want to add a sub-item so we update the options directly.
|
||||
idx: int = handler.flow_state["_idx"]
|
||||
handler.options[SENSOR_DOMAIN][idx].update(user_input)
|
||||
_strip_sentinel(handler.options[SENSOR_DOMAIN][idx])
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -125,5 +125,72 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_class": {
|
||||
"options": {
|
||||
"none": "No device class",
|
||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"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_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::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%]",
|
||||
"distance": "[%key:component::sensor::entity_component::distance::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%]",
|
||||
"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_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::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%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::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%]"
|
||||
}
|
||||
},
|
||||
"state_class": {
|
||||
"options": {
|
||||
"none": "No state class",
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"unit_of_measurement": {
|
||||
"options": {
|
||||
"none": "No unit of measurement"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,13 +591,20 @@ class SonosSpeaker:
|
||||
self.async_write_entity_states()
|
||||
self.hass.async_create_task(self.async_subscribe())
|
||||
|
||||
async def async_check_activity(self, now: datetime.datetime) -> None:
|
||||
@callback
|
||||
def async_check_activity(self, now: datetime.datetime) -> None:
|
||||
"""Validate availability of the speaker based on recent activity."""
|
||||
if not self.available:
|
||||
return
|
||||
if time.monotonic() - self._last_activity < AVAILABILITY_TIMEOUT:
|
||||
return
|
||||
# Ensure the ping is canceled at shutdown
|
||||
self.hass.async_create_background_task(
|
||||
self._async_check_activity(), f"sonos {self.uid} {self.zone_name} ping"
|
||||
)
|
||||
|
||||
async def _async_check_activity(self) -> None:
|
||||
"""Validate availability of the speaker based on recent activity."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.ping)
|
||||
except SonosUpdateError:
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_DISARMING
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -83,18 +84,24 @@ class VerisureAlarm(
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
self._attr_state = STATE_ALARM_DISARMING
|
||||
self.async_write_ha_state()
|
||||
await self._async_set_arm_state(
|
||||
"DISARMED", self.coordinator.verisure.disarm(code)
|
||||
)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
self._attr_state = STATE_ALARM_ARMING
|
||||
self.async_write_ha_state()
|
||||
await self._async_set_arm_state(
|
||||
"ARMED_HOME", self.coordinator.verisure.arm_home(code)
|
||||
)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
self._attr_state = STATE_ALARM_ARMING
|
||||
self.async_write_ha_state()
|
||||
await self._async_set_arm_state(
|
||||
"ARMED_AWAY", self.coordinator.verisure.arm_away(code)
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_longer_in_range": "The lock is no longer in Bluetooth range. Move the lock or adapter and again.",
|
||||
"no_longer_in_range": "The lock is no longer in Bluetooth range. Move the lock or adapter and try again.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any
|
||||
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.const import STATE_ON, EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -164,6 +167,36 @@ class IASZone(BinarySensor):
|
||||
"""Parse the raw attribute into a bool state."""
|
||||
return BinarySensor.parse(value & 3) # use only bit 0 and 1 for alarm state
|
||||
|
||||
# temporary code to migrate old IasZone sensors to update attribute cache state once
|
||||
# remove in 2024.4.0
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return state attributes."""
|
||||
return {"migrated_to_cache": True} # writing new state means we're migrated
|
||||
|
||||
# temporary migration code
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state."""
|
||||
# trigger migration if extra state attribute is not present
|
||||
if "migrated_to_cache" not in last_state.attributes:
|
||||
self.migrate_to_zigpy_cache(last_state)
|
||||
|
||||
# temporary migration code
|
||||
@callback
|
||||
def migrate_to_zigpy_cache(self, last_state):
|
||||
"""Save old IasZone sensor state to attribute cache."""
|
||||
# previous HA versions did not update the attribute cache for IasZone sensors, so do it once here
|
||||
# a HA state write is triggered shortly afterwards and writes the "migrated_to_cache" extra state attribute
|
||||
if last_state.state == STATE_ON:
|
||||
migrated_state = IasZone.ZoneStatus.Alarm_1
|
||||
else:
|
||||
migrated_state = IasZone.ZoneStatus(0)
|
||||
|
||||
self._channel.cluster.update_attribute(
|
||||
IasZone.attributes_by_name[self.SENSOR_ATTR].id, migrated_state
|
||||
)
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
|
||||
@@ -58,15 +58,19 @@ class AttrReportConfig(TypedDict, total=True):
|
||||
|
||||
def parse_and_log_command(channel, tsn, command_id, args):
|
||||
"""Parse and log a zigbee cluster command."""
|
||||
cmd = channel.cluster.server_commands.get(command_id, [command_id])[0]
|
||||
try:
|
||||
name = channel.cluster.server_commands[command_id].name
|
||||
except KeyError:
|
||||
name = f"0x{command_id:02X}"
|
||||
|
||||
channel.debug(
|
||||
"received '%s' command with %s args on cluster_id '%s' tsn '%s'",
|
||||
cmd,
|
||||
name,
|
||||
args,
|
||||
channel.cluster.cluster_id,
|
||||
tsn,
|
||||
)
|
||||
return cmd
|
||||
return name
|
||||
|
||||
|
||||
def decorate_command(channel, command):
|
||||
|
||||
@@ -137,6 +137,8 @@ CONF_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state"
|
||||
CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join"
|
||||
CONF_ENABLE_QUIRKS = "enable_quirks"
|
||||
CONF_FLOWCONTROL = "flow_control"
|
||||
CONF_NWK = "network"
|
||||
CONF_NWK_CHANNEL = "channel"
|
||||
CONF_RADIO_TYPE = "radio_type"
|
||||
CONF_USB_PATH = "usb_path"
|
||||
CONF_USE_THREAD = "use_thread"
|
||||
|
||||
@@ -41,6 +41,8 @@ from .const import (
|
||||
ATTR_TYPE,
|
||||
CONF_DATABASE,
|
||||
CONF_DEVICE_PATH,
|
||||
CONF_NWK,
|
||||
CONF_NWK_CHANNEL,
|
||||
CONF_RADIO_TYPE,
|
||||
CONF_USE_THREAD,
|
||||
CONF_ZIGPY,
|
||||
@@ -172,6 +174,20 @@ class ZHAGateway:
|
||||
):
|
||||
app_config[CONF_USE_THREAD] = False
|
||||
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
is_multiprotocol_url,
|
||||
)
|
||||
|
||||
# Until we have a way to coordinate channels with the Thread half of multi-PAN,
|
||||
# stick to the old zigpy default of channel 15 instead of dynamically scanning
|
||||
if (
|
||||
is_multiprotocol_url(app_config[CONF_DEVICE][CONF_DEVICE_PATH])
|
||||
and app_config.get(CONF_NWK, {}).get(CONF_NWK_CHANNEL) is None
|
||||
):
|
||||
app_config.setdefault(CONF_NWK, {})[CONF_NWK_CHANNEL] = 15
|
||||
|
||||
return app_controller_cls, app_controller_cls.SCHEMA(app_config)
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
|
||||
from zigpy.profiles import PROFILES
|
||||
from zigpy.types import Channels
|
||||
from zigpy.zcl import Cluster
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
@@ -67,11 +68,19 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
config: dict = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
|
||||
gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
|
||||
energy_scan = await gateway.application_controller.energy_scan(
|
||||
channels=Channels.ALL_CHANNELS, duration_exp=4, count=1
|
||||
)
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
"config": config,
|
||||
"config_entry": config_entry.as_dict(),
|
||||
"application_state": shallow_asdict(gateway.application_controller.state),
|
||||
"energy_scan": {
|
||||
channel: 100 * energy / 255 for channel, energy in energy_scan.items()
|
||||
},
|
||||
"versions": {
|
||||
"bellows": version("bellows"),
|
||||
"zigpy": version("zigpy"),
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
"zigpy_znp"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.34.10",
|
||||
"bellows==0.35.0",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.95",
|
||||
"zigpy-deconz==0.19.2",
|
||||
"zigpy==0.53.2",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-deconz==0.20.0",
|
||||
"zigpy==0.54.0",
|
||||
"zigpy-xbee==0.17.0",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.3"
|
||||
"zigpy-znp==0.10.0"
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zwave_js_server"],
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.47.1"],
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.47.3"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "0658",
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0b1"
|
||||
PATCH_VERSION: Final = "0b3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
||||
@@ -25,7 +25,7 @@ ha-av==10.0.0
|
||||
hass-nabucasa==0.63.1
|
||||
hassil==1.0.6
|
||||
home-assistant-bluetooth==1.9.3
|
||||
home-assistant-frontend==20230329.0
|
||||
home-assistant-frontend==20230331.0
|
||||
home-assistant-intents==2023.3.29
|
||||
httpx==0.23.3
|
||||
ifaddr==0.1.7
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.4.0b1"
|
||||
version = "2023.4.0b3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -422,7 +422,7 @@ beautifulsoup4==4.11.1
|
||||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.10
|
||||
bellows==0.35.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.0
|
||||
@@ -907,7 +907,7 @@ hole==0.8.0
|
||||
holidays==0.21.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230329.0
|
||||
home-assistant-frontend==20230331.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.3.29
|
||||
@@ -2234,7 +2234,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.12
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.5.8
|
||||
reolink-aio==0.5.9
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@@ -2710,25 +2710,25 @@ zhong_hong_hvac==1.0.9
|
||||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.2
|
||||
zigpy-deconz==0.20.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.16.2
|
||||
zigpy-xbee==0.17.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.3
|
||||
zigpy-znp==0.10.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.53.2
|
||||
zigpy==0.54.0
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.47.1
|
||||
zwave-js-server-python==0.47.3
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.6
|
||||
|
||||
@@ -355,7 +355,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.11.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.10
|
||||
bellows==0.35.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.0
|
||||
@@ -693,7 +693,7 @@ hole==0.8.0
|
||||
holidays==0.21.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230329.0
|
||||
home-assistant-frontend==20230331.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.3.29
|
||||
@@ -1597,7 +1597,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.12
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.5.8
|
||||
reolink-aio==0.5.9
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@@ -1938,22 +1938,22 @@ zeversolar==0.3.1
|
||||
zha-quirks==0.0.95
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.2
|
||||
zigpy-deconz==0.20.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.16.2
|
||||
zigpy-xbee==0.17.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.3
|
||||
zigpy-znp==0.10.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.53.2
|
||||
zigpy==0.54.0
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.47.1
|
||||
zwave-js-server-python==0.47.3
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.6
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -224,12 +225,16 @@ async def test_with_invalid_code(hass: HomeAssistant, service, expected_state) -
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
|
||||
|
||||
await hass.services.async_call(
|
||||
alarm_control_panel.DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: CODE + "2"},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"):
|
||||
await hass.services.async_call(
|
||||
alarm_control_panel.DOMAIN,
|
||||
service,
|
||||
{
|
||||
ATTR_ENTITY_ID: "alarm_control_panel.test",
|
||||
ATTR_CODE: f"{CODE}2",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
|
||||
|
||||
@@ -1082,7 +1087,8 @@ async def test_disarm_during_trigger_with_invalid_code(hass: HomeAssistant) -> N
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
|
||||
|
||||
await common.async_alarm_disarm(hass, entity_id=entity_id)
|
||||
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"):
|
||||
await common.async_alarm_disarm(hass, entity_id=entity_id)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
|
||||
|
||||
@@ -1125,7 +1131,8 @@ async def test_disarm_with_template_code(hass: HomeAssistant) -> None:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ALARM_ARMED_HOME
|
||||
|
||||
await common.async_alarm_disarm(hass, "def")
|
||||
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"):
|
||||
await common.async_alarm_disarm(hass, "def")
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ALARM_ARMED_HOME
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -280,12 +281,13 @@ async def test_with_invalid_code(
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
|
||||
|
||||
await hass.services.async_call(
|
||||
alarm_control_panel.DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: f"{CODE}2"},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"):
|
||||
await hass.services.async_call(
|
||||
alarm_control_panel.DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: f"{CODE}2"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
|
||||
|
||||
@@ -881,7 +883,8 @@ async def test_disarm_during_trigger_with_invalid_code(
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
|
||||
|
||||
await common.async_alarm_disarm(hass, entity_id=entity_id)
|
||||
with pytest.raises(HomeAssistantError, match=r"Invalid alarm code provided$"):
|
||||
await common.async_alarm_disarm(hass, entity_id=entity_id)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
|
||||
|
||||
@@ -1307,7 +1310,8 @@ async def test_disarm_with_template_code(
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ALARM_ARMED_HOME
|
||||
|
||||
await common.async_alarm_disarm(hass, "def")
|
||||
with pytest.raises(HomeAssistantError, match=r"Invalid alarm code provided$"):
|
||||
await common.async_alarm_disarm(hass, "def")
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ALARM_ARMED_HOME
|
||||
|
||||
@@ -671,6 +671,19 @@ async def test_migrate_events_context_ids(
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=None,
|
||||
),
|
||||
Events(
|
||||
event_type="garbage_context_id_event",
|
||||
event_data=None,
|
||||
origin_idx=0,
|
||||
time_fired=None,
|
||||
time_fired_ts=1677721632.552529,
|
||||
context_id="adapt_lgt:b'5Cf*':interval:b'0R'",
|
||||
context_id_bin=None,
|
||||
context_user_id=None,
|
||||
context_user_id_bin=None,
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=None,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -695,12 +708,13 @@ async def test_migrate_events_context_ids(
|
||||
"empty_context_id_event",
|
||||
"ulid_context_id_event",
|
||||
"invalid_context_id_event",
|
||||
"garbage_context_id_event",
|
||||
]
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
assert len(events) == 4
|
||||
assert len(events) == 5
|
||||
return {event.event_type: _object_as_dict(event) for event in events}
|
||||
|
||||
events_by_type = await instance.async_add_executor_job(_fetch_migrated_events)
|
||||
@@ -746,6 +760,14 @@ async def test_migrate_events_context_ids(
|
||||
assert invalid_context_id_event["context_user_id_bin"] is None
|
||||
assert invalid_context_id_event["context_parent_id_bin"] is None
|
||||
|
||||
garbage_context_id_event = events_by_type["garbage_context_id_event"]
|
||||
assert garbage_context_id_event["context_id"] is None
|
||||
assert garbage_context_id_event["context_user_id"] is None
|
||||
assert garbage_context_id_event["context_parent_id"] is None
|
||||
assert garbage_context_id_event["context_id_bin"] == b"\x00" * 16
|
||||
assert garbage_context_id_event["context_user_id_bin"] is None
|
||||
assert garbage_context_id_event["context_parent_id_bin"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_migrate_context_ids", [True])
|
||||
async def test_migrate_states_context_ids(
|
||||
@@ -803,6 +825,16 @@ async def test_migrate_states_context_ids(
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=None,
|
||||
),
|
||||
States(
|
||||
entity_id="state.garbage_context_id",
|
||||
last_updated_ts=1677721632.552529,
|
||||
context_id="adapt_lgt:b'5Cf*':interval:b'0R'",
|
||||
context_id_bin=None,
|
||||
context_user_id=None,
|
||||
context_user_id_bin=None,
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=None,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -827,12 +859,13 @@ async def test_migrate_states_context_ids(
|
||||
"state.empty_context_id",
|
||||
"state.ulid_context_id",
|
||||
"state.invalid_context_id",
|
||||
"state.garbage_context_id",
|
||||
]
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
assert len(events) == 4
|
||||
assert len(events) == 5
|
||||
return {state.entity_id: _object_as_dict(state) for state in events}
|
||||
|
||||
states_by_entity_id = await instance.async_add_executor_job(_fetch_migrated_states)
|
||||
@@ -877,6 +910,14 @@ async def test_migrate_states_context_ids(
|
||||
assert invalid_context_id["context_user_id_bin"] is None
|
||||
assert invalid_context_id["context_parent_id_bin"] is None
|
||||
|
||||
garbage_context_id = states_by_entity_id["state.garbage_context_id"]
|
||||
assert garbage_context_id["context_id"] is None
|
||||
assert garbage_context_id["context_user_id"] is None
|
||||
assert garbage_context_id["context_parent_id"] is None
|
||||
assert garbage_context_id["context_id_bin"] == b"\x00" * 16
|
||||
assert garbage_context_id["context_user_id_bin"] is None
|
||||
assert garbage_context_id["context_parent_id_bin"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_migrate_event_type_ids", [True])
|
||||
async def test_migrate_event_type_ids(
|
||||
@@ -957,7 +998,7 @@ async def test_migrate_entity_ids(
|
||||
instance = await async_setup_recorder_instance(hass)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
def _insert_events():
|
||||
def _insert_states():
|
||||
with session_scope(hass=hass) as session:
|
||||
session.add_all(
|
||||
(
|
||||
@@ -979,7 +1020,7 @@ async def test_migrate_entity_ids(
|
||||
)
|
||||
)
|
||||
|
||||
await instance.async_add_executor_job(_insert_events)
|
||||
await instance.async_add_executor_job(_insert_states)
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
# This is a threadsafe way to add a task to the recorder
|
||||
@@ -1065,3 +1106,149 @@ async def test_post_migrate_entity_ids(
|
||||
assert states_by_state["one_1"] is None
|
||||
assert states_by_state["two_2"] is None
|
||||
assert states_by_state["two_1"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_migrate_entity_ids", [True])
|
||||
async def test_migrate_null_entity_ids(
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test we can migrate entity_ids to the StatesMeta table."""
|
||||
instance = await async_setup_recorder_instance(hass)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
def _insert_states():
|
||||
with session_scope(hass=hass) as session:
|
||||
session.add(
|
||||
States(
|
||||
entity_id="sensor.one",
|
||||
state="one_1",
|
||||
last_updated_ts=1.452529,
|
||||
),
|
||||
)
|
||||
session.add_all(
|
||||
States(
|
||||
entity_id=None,
|
||||
state="empty",
|
||||
last_updated_ts=time + 1.452529,
|
||||
)
|
||||
for time in range(1000)
|
||||
)
|
||||
session.add(
|
||||
States(
|
||||
entity_id="sensor.one",
|
||||
state="one_1",
|
||||
last_updated_ts=2.452529,
|
||||
),
|
||||
)
|
||||
|
||||
await instance.async_add_executor_job(_insert_states)
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
# This is a threadsafe way to add a task to the recorder
|
||||
instance.queue_task(EntityIDMigrationTask())
|
||||
await async_recorder_block_till_done(hass)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
def _fetch_migrated_states():
|
||||
with session_scope(hass=hass) as session:
|
||||
states = (
|
||||
session.query(
|
||||
States.state,
|
||||
States.metadata_id,
|
||||
States.last_updated_ts,
|
||||
StatesMeta.entity_id,
|
||||
)
|
||||
.outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id)
|
||||
.all()
|
||||
)
|
||||
assert len(states) == 1002
|
||||
result = {}
|
||||
for state in states:
|
||||
result.setdefault(state.entity_id, []).append(
|
||||
{
|
||||
"state_id": state.entity_id,
|
||||
"last_updated_ts": state.last_updated_ts,
|
||||
"state": state.state,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
states_by_entity_id = await instance.async_add_executor_job(_fetch_migrated_states)
|
||||
assert len(states_by_entity_id[migration._EMPTY_ENTITY_ID]) == 1000
|
||||
assert len(states_by_entity_id["sensor.one"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_migrate_event_type_ids", [True])
|
||||
async def test_migrate_null_event_type_ids(
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test we can migrate event_types to the EventTypes table when the event_type is NULL."""
|
||||
instance = await async_setup_recorder_instance(hass)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
def _insert_events():
|
||||
with session_scope(hass=hass) as session:
|
||||
session.add(
|
||||
Events(
|
||||
event_type="event_type_one",
|
||||
origin_idx=0,
|
||||
time_fired_ts=1.452529,
|
||||
),
|
||||
)
|
||||
session.add_all(
|
||||
Events(
|
||||
event_type=None,
|
||||
origin_idx=0,
|
||||
time_fired_ts=time + 1.452529,
|
||||
)
|
||||
for time in range(1000)
|
||||
)
|
||||
session.add(
|
||||
Events(
|
||||
event_type="event_type_one",
|
||||
origin_idx=0,
|
||||
time_fired_ts=2.452529,
|
||||
),
|
||||
)
|
||||
|
||||
await instance.async_add_executor_job(_insert_events)
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
# This is a threadsafe way to add a task to the recorder
|
||||
|
||||
instance.queue_task(EventTypeIDMigrationTask())
|
||||
await async_recorder_block_till_done(hass)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
def _fetch_migrated_events():
|
||||
with session_scope(hass=hass) as session:
|
||||
events = (
|
||||
session.query(Events.event_id, Events.time_fired, EventTypes.event_type)
|
||||
.filter(
|
||||
Events.event_type_id.in_(
|
||||
select_event_type_ids(
|
||||
(
|
||||
"event_type_one",
|
||||
migration._EMPTY_EVENT_TYPE,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.outerjoin(EventTypes, Events.event_type_id == EventTypes.event_type_id)
|
||||
.all()
|
||||
)
|
||||
assert len(events) == 1002
|
||||
result = {}
|
||||
for event in events:
|
||||
result.setdefault(event.event_type, []).append(
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
"time_fired": event.time_fired,
|
||||
"event_type": event.event_type,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
events_by_type = await instance.async_add_executor_job(_fetch_migrated_events)
|
||||
assert len(events_by_type["event_type_one"]) == 2
|
||||
assert len(events_by_type[migration._EMPTY_EVENT_TYPE]) == 1000
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Fixtures for the Scrape integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
@@ -32,6 +33,16 @@ from . import MockRestData
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Automatically path uuid generator."""
|
||||
with patch(
|
||||
"homeassistant.components.scrape.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="get_config")
|
||||
async def get_config_to_integration_load() -> dict[str, Any]:
|
||||
"""Return default minimal configuration.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""Test the Scrape config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
import uuid
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.rest.data import DEFAULT_TIMEOUT
|
||||
from homeassistant.components.rest.schema import DEFAULT_METHOD
|
||||
from homeassistant.components.scrape import DOMAIN
|
||||
from homeassistant.components.scrape.config_flow import NONE_SENTINEL
|
||||
from homeassistant.components.scrape.const import (
|
||||
CONF_ENCODING,
|
||||
CONF_INDEX,
|
||||
@@ -15,14 +16,18 @@ from homeassistant.components.scrape.const import (
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_METHOD,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_RESOURCE,
|
||||
CONF_TIMEOUT,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -34,7 +39,9 @@ from . import MockRestData
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
async def test_form(
|
||||
hass: HomeAssistant, get_data: MockRestData, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -46,10 +53,7 @@ async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
with patch(
|
||||
"homeassistant.components.rest.RestData",
|
||||
return_value=get_data,
|
||||
) as mock_data, patch(
|
||||
"homeassistant.components.scrape.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
) as mock_data:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@@ -66,6 +70,9 @@ async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
CONF_NAME: "Current version",
|
||||
CONF_SELECT: ".current-version h1",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_DEVICE_CLASS: NONE_SENTINEL,
|
||||
CONF_STATE_CLASS: NONE_SENTINEL,
|
||||
CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -92,7 +99,9 @@ async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
async def test_flow_fails(
|
||||
hass: HomeAssistant, get_data: MockRestData, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test config flow error."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -137,9 +146,6 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
with patch(
|
||||
"homeassistant.components.rest.RestData",
|
||||
return_value=get_data,
|
||||
), patch(
|
||||
"homeassistant.components.scrape.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -157,6 +163,9 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
|
||||
CONF_NAME: "Current version",
|
||||
CONF_SELECT: ".current-version h1",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_DEVICE_CLASS: NONE_SENTINEL,
|
||||
CONF_STATE_CLASS: NONE_SENTINEL,
|
||||
CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -278,6 +287,9 @@ async def test_options_add_remove_sensor_flow(
|
||||
CONF_NAME: "Template",
|
||||
CONF_SELECT: "template",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_DEVICE_CLASS: NONE_SENTINEL,
|
||||
CONF_STATE_CLASS: NONE_SENTINEL,
|
||||
CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -405,6 +417,9 @@ async def test_options_edit_sensor_flow(
|
||||
user_input={
|
||||
CONF_SELECT: "template",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_DEVICE_CLASS: NONE_SENTINEL,
|
||||
CONF_STATE_CLASS: NONE_SENTINEL,
|
||||
CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -434,3 +449,161 @@ async def test_options_edit_sensor_flow(
|
||||
# Check the state of the entity has changed as expected
|
||||
state = hass.states.get("sensor.current_version")
|
||||
assert state.state == "Trying to get"
|
||||
|
||||
|
||||
async def test_sensor_options_add_device_class(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test options flow to edit a sensor."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
CONF_RESOURCE: "https://www.home-assistant.io",
|
||||
CONF_METHOD: DEFAULT_METHOD,
|
||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||
CONF_TIMEOUT: DEFAULT_TIMEOUT,
|
||||
CONF_ENCODING: DEFAULT_ENCODING,
|
||||
"sensor": [
|
||||
{
|
||||
CONF_NAME: "Current Temp",
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_INDEX: 0,
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
|
||||
}
|
||||
],
|
||||
},
|
||||
entry_id="1",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_edit_sensor"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "select_edit_sensor"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"index": "0"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "edit_sensor"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_DEVICE_CLASS: "temperature",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
CONF_UNIT_OF_MEASUREMENT: "°C",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_RESOURCE: "https://www.home-assistant.io",
|
||||
CONF_METHOD: "GET",
|
||||
CONF_VERIFY_SSL: True,
|
||||
CONF_TIMEOUT: 10,
|
||||
CONF_ENCODING: "UTF-8",
|
||||
"sensor": [
|
||||
{
|
||||
CONF_NAME: "Current Temp",
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_INDEX: 0,
|
||||
CONF_DEVICE_CLASS: "temperature",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
CONF_UNIT_OF_MEASUREMENT: "°C",
|
||||
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_sensor_options_remove_device_class(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test options flow to edit a sensor."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
CONF_RESOURCE: "https://www.home-assistant.io",
|
||||
CONF_METHOD: DEFAULT_METHOD,
|
||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||
CONF_TIMEOUT: DEFAULT_TIMEOUT,
|
||||
CONF_ENCODING: DEFAULT_ENCODING,
|
||||
"sensor": [
|
||||
{
|
||||
CONF_NAME: "Current Temp",
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_INDEX: 0,
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_DEVICE_CLASS: "temperature",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
CONF_UNIT_OF_MEASUREMENT: "°C",
|
||||
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
|
||||
}
|
||||
],
|
||||
},
|
||||
entry_id="1",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_edit_sensor"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "select_edit_sensor"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"index": "0"},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "edit_sensor"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_INDEX: 0.0,
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_DEVICE_CLASS: NONE_SENTINEL,
|
||||
CONF_STATE_CLASS: NONE_SENTINEL,
|
||||
CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_RESOURCE: "https://www.home-assistant.io",
|
||||
CONF_METHOD: "GET",
|
||||
CONF_VERIFY_SSL: True,
|
||||
CONF_TIMEOUT: 10,
|
||||
CONF_ENCODING: "UTF-8",
|
||||
"sensor": [
|
||||
{
|
||||
CONF_NAME: "Current Temp",
|
||||
CONF_SELECT: ".current-temp h3",
|
||||
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
|
||||
CONF_INDEX: 0,
|
||||
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Test ZHA base channel module."""
|
||||
|
||||
from homeassistant.components.zha.core.channels.base import parse_and_log_command
|
||||
|
||||
from tests.components.zha.test_channels import ( # noqa: F401
|
||||
channel_pool,
|
||||
poll_control_ch,
|
||||
zigpy_coordinator_device,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_and_log_command(poll_control_ch): # noqa: F811
|
||||
"""Test that `parse_and_log_command` correctly parses a known command."""
|
||||
assert parse_and_log_command(poll_control_ch, 0x00, 0x01, []) == "fast_poll_stop"
|
||||
|
||||
|
||||
def test_parse_and_log_command_unknown(poll_control_ch): # noqa: F811
|
||||
"""Test that `parse_and_log_command` correctly parses an unknown command."""
|
||||
assert parse_and_log_command(poll_control_ch, 0x00, 0xAB, []) == "0xAB"
|
||||
@@ -8,12 +8,15 @@ import zigpy.zcl.clusters.security as security
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import restore_state
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .common import (
|
||||
async_enable_traffic,
|
||||
async_test_rejoin,
|
||||
find_entity_id,
|
||||
send_attributes_report,
|
||||
update_attribute_cache,
|
||||
)
|
||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||
|
||||
@@ -120,3 +123,92 @@ async def test_binary_sensor(
|
||||
# test rejoin
|
||||
await async_test_rejoin(hass, zigpy_device, [cluster], reporting)
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def core_rs(hass_storage):
|
||||
"""Core.restore_state fixture."""
|
||||
|
||||
def _storage(entity_id, attributes, state):
|
||||
now = dt_util.utcnow().isoformat()
|
||||
|
||||
hass_storage[restore_state.STORAGE_KEY] = {
|
||||
"version": restore_state.STORAGE_VERSION,
|
||||
"key": restore_state.STORAGE_KEY,
|
||||
"data": [
|
||||
{
|
||||
"state": {
|
||||
"entity_id": entity_id,
|
||||
"state": str(state),
|
||||
"attributes": attributes,
|
||||
"last_changed": now,
|
||||
"last_updated": now,
|
||||
"context": {
|
||||
"id": "3c2243ff5f30447eb12e7348cfd5b8ff",
|
||||
"user_id": None,
|
||||
},
|
||||
},
|
||||
"last_seen": now,
|
||||
}
|
||||
],
|
||||
}
|
||||
return
|
||||
|
||||
return _storage
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"restored_state",
|
||||
[
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
],
|
||||
)
|
||||
async def test_binary_sensor_migration_not_migrated(
|
||||
hass: HomeAssistant,
|
||||
zigpy_device_mock,
|
||||
core_rs,
|
||||
zha_device_restored,
|
||||
restored_state,
|
||||
) -> None:
|
||||
"""Test temporary ZHA IasZone binary_sensor migration to zigpy cache."""
|
||||
|
||||
entity_id = "binary_sensor.fakemanufacturer_fakemodel_iaszone"
|
||||
core_rs(entity_id, state=restored_state, attributes={}) # migration sensor state
|
||||
|
||||
zigpy_device = zigpy_device_mock(DEVICE_IAS)
|
||||
zha_device = await zha_device_restored(zigpy_device)
|
||||
entity_id = await find_entity_id(Platform.BINARY_SENSOR, zha_device, hass)
|
||||
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == restored_state
|
||||
|
||||
# confirm migration extra state attribute was set to True
|
||||
assert hass.states.get(entity_id).attributes["migrated_to_cache"]
|
||||
|
||||
|
||||
async def test_binary_sensor_migration_already_migrated(
|
||||
hass: HomeAssistant,
|
||||
zigpy_device_mock,
|
||||
core_rs,
|
||||
zha_device_restored,
|
||||
) -> None:
|
||||
"""Test temporary ZHA IasZone binary_sensor migration doesn't migrate multiple times."""
|
||||
|
||||
entity_id = "binary_sensor.fakemanufacturer_fakemodel_iaszone"
|
||||
core_rs(entity_id, state=STATE_OFF, attributes={"migrated_to_cache": True})
|
||||
|
||||
zigpy_device = zigpy_device_mock(DEVICE_IAS)
|
||||
|
||||
cluster = zigpy_device.endpoints.get(1).ias_zone
|
||||
cluster.PLUGGED_ATTR_READS = {
|
||||
"zone_status": security.IasZone.ZoneStatus.Alarm_1,
|
||||
}
|
||||
update_attribute_cache(cluster)
|
||||
|
||||
zha_device = await zha_device_restored(zigpy_device)
|
||||
entity_id = await find_entity_id(Platform.BINARY_SENSOR, zha_device, hass)
|
||||
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == STATE_ON # matches attribute cache
|
||||
assert hass.states.get(entity_id).attributes["migrated_to_cache"]
|
||||
|
||||
@@ -6,6 +6,7 @@ import zigpy.profiles.zha as zha
|
||||
import zigpy.zcl.clusters.security as security
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED
|
||||
from homeassistant.components.zha.core.const import DATA_ZHA, DATA_ZHA_GATEWAY
|
||||
from homeassistant.components.zha.core.device import ZHADevice
|
||||
from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT
|
||||
from homeassistant.const import Platform
|
||||
@@ -62,14 +63,25 @@ async def test_diagnostics_for_config_entry(
|
||||
) -> None:
|
||||
"""Test diagnostics for config entry."""
|
||||
await zha_device_joined(zigpy_device)
|
||||
diagnostics_data = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
)
|
||||
assert diagnostics_data
|
||||
|
||||
gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
scan = {c: c for c in range(11, 26 + 1)}
|
||||
|
||||
with patch.object(gateway.application_controller, "energy_scan", return_value=scan):
|
||||
diagnostics_data = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
)
|
||||
|
||||
for key in CONFIG_ENTRY_DIAGNOSTICS_KEYS:
|
||||
assert key in diagnostics_data
|
||||
assert diagnostics_data[key] is not None
|
||||
|
||||
# Energy scan results are presented as a percentage. JSON object keys also must be
|
||||
# strings, not integers.
|
||||
assert diagnostics_data["energy_scan"] == {
|
||||
str(k): 100 * v / 255 for k, v in scan.items()
|
||||
}
|
||||
|
||||
|
||||
async def test_diagnostics_for_device(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -323,3 +323,32 @@ async def test_gateway_initialize_bellows_thread(
|
||||
await zha_gateway.async_initialize()
|
||||
|
||||
assert mock_new.mock_calls[0].args[0]["use_thread"] is thread_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_path", "config_override", "expected_channel"),
|
||||
[
|
||||
("/dev/ttyUSB0", {}, None),
|
||||
("socket://192.168.1.123:9999", {}, None),
|
||||
("socket://192.168.1.123:9999", {"network": {"channel": 20}}, 20),
|
||||
("socket://core-silabs-multiprotocol:9999", {}, 15),
|
||||
("socket://core-silabs-multiprotocol:9999", {"network": {"channel": 20}}, 20),
|
||||
],
|
||||
)
|
||||
async def test_gateway_force_multi_pan_channel(
|
||||
device_path: str,
|
||||
config_override: dict,
|
||||
expected_channel: int | None,
|
||||
hass: HomeAssistant,
|
||||
coordinator,
|
||||
) -> None:
|
||||
"""Test ZHA disabling the UART thread when connecting to a TCP coordinator."""
|
||||
zha_gateway = get_zha_gateway(hass)
|
||||
assert zha_gateway is not None
|
||||
|
||||
zha_gateway.config_entry.data = dict(zha_gateway.config_entry.data)
|
||||
zha_gateway.config_entry.data["device"]["path"] = device_path
|
||||
zha_gateway._config.setdefault("zigpy_config", {}).update(config_override)
|
||||
|
||||
_, config = zha_gateway.get_application_controller_data()
|
||||
assert config["network"]["channel"] == expected_channel
|
||||
|
||||
Reference in New Issue
Block a user