Compare commits

...

19 Commits

Author SHA1 Message Date
Paulus Schoutsen 03f085d7be Bumped version to 2023.4.0b3 2023-03-31 15:41:37 -04:00
Raman Gupta b3348c3e6f Bump zwave-js-server-python to 0.47.3 (#90606)
* Bump zwave-js-server-python to 0.47.2

* Bump zwave-js-server-python to 0.47.3
2023-03-31 15:41:33 -04:00
puddly 590db0fa74 Perform an energy scan when downloading ZHA diagnostics (#90605) 2023-03-31 15:41:32 -04:00
puddly f56ccf90d9 Fix ZHA definition error on received command (#90602)
* Fix use of deprecated command schema access

* Add a unit test
2023-03-31 15:41:31 -04:00
Bram Kragten c63f8e714e Update frontend to 20230331.0 (#90594) 2023-03-31 15:41:30 -04:00
starkillerOG a20771f571 Bump reolink-aio to 0.5.9 (#90590) 2023-03-31 15:41:29 -04:00
Franck Nijhof 2d482f1f57 Raise on invalid (dis)arm code in manual mqtt alarm (#90584) 2023-03-31 15:41:28 -04:00
Erik Montnemery 499962f4ee Tweak yalexs_ble translations (#90582) 2023-03-31 15:41:27 -04:00
Franck Nijhof 88a407361c Raise on invalid (dis)arm code in manual alarm (#90579) 2023-03-31 15:41:26 -04:00
Franck Nijhof 89dc6db5a7 Add arming/disarming state to Verisure (#90577) 2023-03-31 15:41:25 -04:00
J. Nick Koston de9e7e47fe Make sonos activity check a background task (#90553)
Ensures the task is canceled at shutdown if the device
is offline and the ping is still in progress
2023-03-31 15:41:24 -04:00
epenet ab66664f20 Allow removal of sensor settings in scrape (#90412)
* Allow removal of sensor settings in scrape

* Adjust

* Adjust

* Add comment

* Simplify

* Simplify

* Adjust

* Don't allow empty string

* Only allow None

* Use default as None

* Use sentinel "none"

* Not needed

* Adjust unit of measurement

* Add translation keys for "none"

* Use translations

* Sort

* Add enum and timestamp

* Use translation references

* Remove default and set suggested_values

* Disallow enum device class

* Adjust tests

* Adjust _strip_sentinel
2023-03-31 15:41:23 -04:00
Paulus Schoutsen e7e2532c68 Bumped version to 2023.4.0b2 2023-03-30 20:55:55 -04:00
puddly 4bf10c01f0 Bump ZHA dependencies (#90547)
* Bump ZHA dependencies

* Ensure the network is formed on channel 15 when multi-PAN is in use
2023-03-30 20:55:37 -04:00
J. Nick Koston aad1f4b766 Handle garbage in the context_id column during migration (#90544)
* Handle garbage in the context_id column during migration

* Update homeassistant/components/recorder/migration.py

* lint
2023-03-30 20:55:36 -04:00
J. Nick Koston e32d89215d Fix migration when encountering a NULL entity_id/event_type (#90542)
* Fix migration when encountering a NULL entity_id/event_type

reported in #beta on discord

* simplify
2023-03-30 20:55:36 -04:00
Franck Nijhof 9478518937 Add entity name translations to LaMetric (#90538)
* Add entity name translations to LaMetric

* Consistency
2023-03-30 20:55:35 -04:00
Bram Kragten 8a99d2a566 Update frontend to 20230330.0 (#90524) 2023-03-30 20:55:34 -04:00
TheJulianJES 38aff23be5 Migrate old ZHA IasZone sensor state to zigpy cache (#90508)
* Migrate old ZHA IasZone sensor state to zigpy cache

* Use correct type for ZoneStatus

* Test that migration happens

* Test that migration only happens once

* Fix parametrize
2023-03-30 20:55:33 -04:00
36 changed files with 875 additions and 164 deletions
@@ -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"]
}
+4 -4
View File
@@ -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(),
+1 -2
View File
@@ -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"
}
}
}
}
+1 -1
View File
@@ -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]:
+30 -14
View File
@@ -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"]
}
+32 -7
View File
@@ -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"
}
}
}
}
+8 -1
View File
@@ -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%]",
+34 -1
View File
@@ -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"),
+5 -5
View File
@@ -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",
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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"
+8 -8
View File
@@ -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
+8 -8
View File
@@ -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
+191 -4
View File
@@ -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
+12 -1
View File
@@ -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.
+183 -10
View File
@@ -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",
},
],
}
+19
View File
@@ -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"]
+16 -4
View File
@@ -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,
+29
View File
@@ -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