This commit is contained in:
Paulus Schoutsen
2023-07-13 12:26:19 -04:00
committed by GitHub
59 changed files with 971 additions and 225 deletions

View File

@@ -182,7 +182,6 @@ omit =
homeassistant/components/crownstone/listeners.py homeassistant/components/crownstone/listeners.py
homeassistant/components/cups/sensor.py homeassistant/components/cups/sensor.py
homeassistant/components/currencylayer/sensor.py homeassistant/components/currencylayer/sensor.py
homeassistant/components/daikin/__init__.py
homeassistant/components/daikin/climate.py homeassistant/components/daikin/climate.py
homeassistant/components/daikin/sensor.py homeassistant/components/daikin/sensor.py
homeassistant/components/daikin/switch.py homeassistant/components/daikin/switch.py

View File

@@ -42,6 +42,7 @@ class BlinkSyncModule(AlarmControlPanelEntity):
_attr_icon = ICON _attr_icon = ICON
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_name = None _attr_name = None
_attr_has_entity_name = True
def __init__(self, data, name, sync): def __init__(self, data, name, sync):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""

View File

@@ -58,13 +58,14 @@ async def async_setup_entry(
class BlinkBinarySensor(BinarySensorEntity): class BlinkBinarySensor(BinarySensorEntity):
"""Representation of a Blink binary sensor.""" """Representation of a Blink binary sensor."""
_attr_has_entity_name = True
def __init__( def __init__(
self, data, camera, description: BinarySensorEntityDescription self, data, camera, description: BinarySensorEntityDescription
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.data = data self.data = data
self.entity_description = description self.entity_description = description
self._attr_name = f"{DOMAIN} {camera} {description.name}"
self._camera = data.cameras[camera] self._camera = data.cameras[camera]
self._attr_unique_id = f"{self._camera.serial}-{description.key}" self._attr_unique_id = f"{self._camera.serial}-{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(

View File

@@ -38,6 +38,7 @@ async def async_setup_entry(
class BlinkCamera(Camera): class BlinkCamera(Camera):
"""An implementation of a Blink Camera.""" """An implementation of a Blink Camera."""
_attr_has_entity_name = True
_attr_name = None _attr_name = None
def __init__(self, data, name, camera): def __init__(self, data, name, camera):

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome", "documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bthome-ble==2.12.0"] "requirements": ["bthome-ble==2.12.1"]
} }

View File

@@ -301,55 +301,55 @@
"name": "Condition 1d", "name": "Condition 1d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"condition_2d": { "condition_2d": {
"name": "Condition 2d", "name": "Condition 2d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"condition_3d": { "condition_3d": {
"name": "Condition 3d", "name": "Condition 3d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"condition_4d": { "condition_4d": {
"name": "Condition 4d", "name": "Condition 4d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"condition_5d": { "condition_5d": {
"name": "Condition 5d", "name": "Condition 5d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"conditioncode_1d": { "conditioncode_1d": {
@@ -371,76 +371,76 @@
"name": "Detailed condition 1d", "name": "Detailed condition 1d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]",
"partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]",
"partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]",
"partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
"partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"conditiondetailed_2d": { "conditiondetailed_2d": {
"name": "Detailed condition 2d", "name": "Detailed condition 2d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]",
"partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]",
"partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]",
"partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
"partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"conditiondetailed_3d": { "conditiondetailed_3d": {
"name": "Detailed condition 3d", "name": "Detailed condition 3d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]",
"partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]",
"partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]",
"partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
"partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"conditiondetailed_4d": { "conditiondetailed_4d": {
"name": "Detailed condition 4d", "name": "Detailed condition 4d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]",
"partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]",
"partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]",
"partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
@@ -455,21 +455,21 @@
"name": "Detailed condition 5d", "name": "Detailed condition 5d",
"state": { "state": {
"clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]",
"partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]",
"partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]",
"partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", "fog": "[%key:component::weather::entity_component::_::state::fog%]",
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", "rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
"partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", "snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" "lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
} }
}, },
"conditionexact_1d": { "conditionexact_1d": {

View File

@@ -15,8 +15,9 @@ from homeassistant.const import (
CONF_UUID, CONF_UUID,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@@ -52,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not daikin_api: if not daikin_api:
return False return False
await async_migrate_unique_id(hass, entry, daikin_api)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@@ -67,7 +70,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
async def daikin_api_setup(hass, host, key, uuid, password): async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password):
"""Create a Daikin instance only once.""" """Create a Daikin instance only once."""
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
@@ -127,3 +130,82 @@ class DaikinApi:
name=info.get("name"), name=info.get("name"),
sw_version=info.get("ver", "").replace("_", "."), sw_version=info.get("ver", "").replace("_", "."),
) )
async def async_migrate_unique_id(
hass: HomeAssistant, config_entry: ConfigEntry, api: DaikinApi
) -> None:
"""Migrate old entry."""
dev_reg = dr.async_get(hass)
old_unique_id = config_entry.unique_id
new_unique_id = api.device.mac
new_name = api.device.values["name"]
@callback
def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
"""Update unique ID of entity entry."""
return update_unique_id(entity_entry, new_unique_id)
if new_unique_id == old_unique_id:
return
# Migrate devices
for device_entry in dr.async_entries_for_config_entry(
dev_reg, config_entry.entry_id
):
for connection in device_entry.connections:
if connection[1] == old_unique_id:
new_connections = {
(CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id))
}
_LOGGER.debug(
"Migrating device %s connections to %s",
device_entry.name,
new_connections,
)
dev_reg.async_update_device(
device_entry.id,
merge_connections=new_connections,
)
if device_entry.name is None:
_LOGGER.debug(
"Migrating device name to %s",
new_name,
)
dev_reg.async_update_device(
device_entry.id,
name=new_name,
)
# Migrate entities
await er.async_migrate_entries(hass, config_entry.entry_id, _update_unique_id)
new_data = {**config_entry.data, KEY_MAC: dr.format_mac(new_unique_id)}
hass.config_entries.async_update_entry(
config_entry, unique_id=new_unique_id, data=new_data
)
@callback
def update_unique_id(
entity_entry: er.RegistryEntry, unique_id: str
) -> dict[str, str] | None:
"""Update unique ID of entity entry."""
if entity_entry.unique_id.startswith(unique_id):
# Already correct, nothing to do
return None
unique_id_parts = entity_entry.unique_id.split("-")
unique_id_parts[0] = unique_id
entity_new_unique_id = "-".join(unique_id_parts)
_LOGGER.debug(
"Migrating entity %s from %s to new id %s",
entity_entry.entity_id,
entity_entry.unique_id,
entity_new_unique_id,
)
return {"new_unique_id": entity_new_unique_id}

View File

@@ -7,6 +7,6 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydaikin"], "loggers": ["pydaikin"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pydaikin==2.9.0"], "requirements": ["pydaikin==2.10.5"],
"zeroconf": ["_dkapi._tcp.local."] "zeroconf": ["_dkapi._tcp.local."]
} }

View File

@@ -42,7 +42,7 @@ async def async_setup_entry(
[ [
DaikinZoneSwitch(daikin_api, zone_id) DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, zone in enumerate(zones) for zone_id, zone in enumerate(zones)
if zone != ("-", "0") if zone[0] != "-"
] ]
) )
if daikin_api.device.support_advanced_modes: if daikin_api.device.support_advanced_modes:

View File

@@ -60,7 +60,6 @@ class ServiceDetails(NamedTuple):
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"), SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"),
"yamaha": ServiceDetails("media_player", "yamaha"), "yamaha": ServiceDetails("media_player", "yamaha"),
"openhome": ServiceDetails("media_player", "openhome"),
"bluesound": ServiceDetails("media_player", "bluesound"), "bluesound": ServiceDetails("media_player", "bluesound"),
} }
@@ -87,6 +86,7 @@ MIGRATED_SERVICE_HANDLERS = [
SERVICE_MOBILE_APP, SERVICE_MOBILE_APP,
SERVICE_NETGEAR, SERVICE_NETGEAR,
SERVICE_OCTOPRINT, SERVICE_OCTOPRINT,
"openhome",
"philips_hue", "philips_hue",
SERVICE_SAMSUNG_PRINTER, SERVICE_SAMSUNG_PRINTER,
"sonos", "sonos",

View File

@@ -76,6 +76,7 @@ class ControllerEntity(ClimateEntity):
_attr_fan_modes = list(_HA_FAN_TO_ESCEA) _attr_fan_modes = list(_HA_FAN_TO_ESCEA)
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_icon = ICON _attr_icon = ICON
_attr_precision = PRECISION_WHOLE _attr_precision = PRECISION_WHOLE

View File

@@ -388,6 +388,7 @@ async def async_setup_entry( # noqa: C901
assert cli.api_version is not None assert cli.api_version is not None
entry_data.api_version = cli.api_version entry_data.api_version = cli.api_version
entry_data.available = True entry_data.available = True
entry_data.expected_disconnect = True
if entry_data.device_info.name: if entry_data.device_info.name:
reconnect_logic.name = entry_data.device_info.name reconnect_logic.name = entry_data.device_info.name

View File

@@ -34,7 +34,7 @@ from .const import (
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
DOMAIN, DOMAIN,
) )
from .dashboard import async_get_dashboard, async_set_dashboard_info from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
@@ -391,7 +391,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
""" """
if ( if (
self._device_name is None self._device_name is None
or (dashboard := async_get_dashboard(self.hass)) is None or (manager := await async_get_or_create_dashboard_manager(self.hass))
is None
or (dashboard := manager.async_get()) is None
): ):
return False return False

View File

@@ -93,13 +93,6 @@ class ESPHomeDashboardManager:
hass, addon_slug, url, async_get_clientsession(hass) hass, addon_slug, url, async_get_clientsession(hass)
) )
await dashboard.async_request_refresh() await dashboard.async_request_refresh()
if not cur_dashboard and not dashboard.last_update_success:
# If there was no previous dashboard and the new one is not available,
# we skip setup and wait for discovery.
_LOGGER.error(
"Dashboard unavailable; skipping setup: %s", dashboard.last_exception
)
return
self._current_dashboard = dashboard self._current_dashboard = dashboard
@@ -143,7 +136,14 @@ class ESPHomeDashboardManager:
@callback @callback
def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None:
"""Get an instance of the dashboard if set.""" """Get an instance of the dashboard if set.
This is only safe to call after `async_setup` has been completed.
It should not be called from the config flow because there is a race
where manager can be an asyncio.Event instead of the actual manager
because the singleton decorator is not yet done.
"""
manager: ESPHomeDashboardManager | None = hass.data.get(KEY_DASHBOARD_MANAGER) manager: ESPHomeDashboardManager | None = hass.data.get(KEY_DASHBOARD_MANAGER)
return manager.async_get() if manager else None return manager.async_get() if manager else None

View File

@@ -30,9 +30,6 @@ async def async_setup_entry(
avm_wrapper.fritz_guest_wifi.get_info avm_wrapper.fritz_guest_wifi.get_info
) )
if not guest_wifi_info.get("NewEnable"):
return
async_add_entities( async_add_entities(
[ [
FritzGuestWifiQRImage( FritzGuestWifiQRImage(

View File

@@ -16,5 +16,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["goalzero"], "loggers": ["goalzero"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["goalzero==0.2.1"] "requirements": ["goalzero==0.2.2"]
} }

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==2.6.5"], "requirements": ["aiohomekit==2.6.7"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@@ -43,6 +43,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE} _attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE}
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
_attr_supported_features = LightEntityFeature.EFFECT _attr_supported_features = LightEntityFeature.EFFECT
def __init__( def __init__(

View File

@@ -22,6 +22,24 @@ PLATFORMS_BULB = [Platform.LIGHT]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def _async_get_device_state(
device: MyStromSwitch | MyStromBulb, ip_address: str
) -> None:
try:
await device.get_state()
except MyStromConnectionError as err:
_LOGGER.error("No route to myStrom plug: %s", ip_address)
raise ConfigEntryNotReady() from err
def _get_mystrom_bulb(host: str, mac: str) -> MyStromBulb:
return MyStromBulb(host, mac)
def _get_mystrom_switch(host: str) -> MyStromSwitch:
return MyStromSwitch(host)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up myStrom from a config entry.""" """Set up myStrom from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
@@ -32,14 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("No route to myStrom plug: %s", host) _LOGGER.error("No route to myStrom plug: %s", host)
raise ConfigEntryNotReady() from err raise ConfigEntryNotReady() from err
info.setdefault("type", 101)
device_type = info["type"] device_type = info["type"]
if device_type in [101, 106, 107]: if device_type in [101, 106, 107]:
device = MyStromSwitch(host) device = _get_mystrom_switch(host)
platforms = PLATFORMS_SWITCH platforms = PLATFORMS_SWITCH
elif device_type == 102: await _async_get_device_state(device, info["ip"])
elif device_type in [102, 105]:
mac = info["mac"] mac = info["mac"]
device = MyStromBulb(host, mac) device = _get_mystrom_bulb(host, mac)
platforms = PLATFORMS_BULB platforms = PLATFORMS_BULB
await _async_get_device_state(device, info["ip"])
if device.bulb_type not in ["rgblamp", "strip"]: if device.bulb_type not in ["rgblamp", "strip"]:
_LOGGER.error( _LOGGER.error(
"Device %s (%s) is not a myStrom bulb nor myStrom LED Strip", "Device %s (%s) is not a myStrom bulb nor myStrom LED Strip",
@@ -51,12 +73,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Unsupported myStrom device type: %s", device_type) _LOGGER.error("Unsupported myStrom device type: %s", device_type)
return False return False
try:
await device.get_state()
except MyStromConnectionError as err:
_LOGGER.error("No route to myStrom plug: %s", info["ip"])
raise ConfigEntryNotReady() from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData( hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData(
device=device, device=device,
info=info, info=info,
@@ -69,10 +85,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
device_type = hass.data[DOMAIN][entry.entry_id].info["type"] device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
platforms = []
if device_type in [101, 106, 107]: if device_type in [101, 106, 107]:
platforms = PLATFORMS_SWITCH platforms.extend(PLATFORMS_SWITCH)
elif device_type == 102: elif device_type in [102, 105]:
platforms = PLATFORMS_BULB platforms.extend(PLATFORMS_BULB)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/python_script", "documentation": "https://www.home-assistant.io/integrations/python_script",
"loggers": ["RestrictedPython"], "loggers": ["RestrictedPython"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["RestrictedPython==6.0"] "requirements": ["RestrictedPython==6.1"]
} }

View File

@@ -2,10 +2,12 @@
from __future__ import annotations from __future__ import annotations
from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController
from pyrainbird.exceptions import RainbirdApiException
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SERIAL_NUMBER from .const import CONF_SERIAL_NUMBER
@@ -29,11 +31,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
) )
) )
try:
model_info = await controller.get_model_and_version()
except RainbirdApiException as err:
raise ConfigEntryNotReady from err
coordinator = RainbirdUpdateCoordinator( coordinator = RainbirdUpdateCoordinator(
hass, hass,
name=entry.title, name=entry.title,
controller=controller, controller=controller,
serial_number=entry.data[CONF_SERIAL_NUMBER], serial_number=entry.data[CONF_SERIAL_NUMBER],
model_info=model_info,
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@@ -9,6 +9,7 @@ from typing import TypeVar
import async_timeout import async_timeout
from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException
from pyrainbird.data import ModelAndVersion
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@@ -42,6 +43,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
name: str, name: str,
controller: AsyncRainbirdController, controller: AsyncRainbirdController,
serial_number: str, serial_number: str,
model_info: ModelAndVersion,
) -> None: ) -> None:
"""Initialize ZoneStateUpdateCoordinator.""" """Initialize ZoneStateUpdateCoordinator."""
super().__init__( super().__init__(
@@ -54,6 +56,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
self._controller = controller self._controller = controller
self._serial_number = serial_number self._serial_number = serial_number
self._zones: set[int] | None = None self._zones: set[int] | None = None
self._model_info = model_info
@property @property
def controller(self) -> AsyncRainbirdController: def controller(self) -> AsyncRainbirdController:
@@ -72,6 +75,8 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
default_name=f"{MANUFACTURER} Controller", default_name=f"{MANUFACTURER} Controller",
identifiers={(DOMAIN, self._serial_number)}, identifiers={(DOMAIN, self._serial_number)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=self._model_info.model_name,
sw_version=f"{self._model_info.major}.{self._model_info.minor}",
) )
async def _async_update_data(self) -> RainbirdDeviceState: async def _async_update_data(self) -> RainbirdDeviceState:

View File

@@ -62,6 +62,7 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity):
"""Define a RainMachine update entity.""" """Define a RainMachine update entity."""
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_name = None
_attr_supported_features = ( _attr_supported_features = (
UpdateEntityFeature.INSTALL UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS | UpdateEntityFeature.PROGRESS

View File

@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.7.1"] "requirements": ["reolink-aio==0.7.3"]
} }

View File

@@ -49,7 +49,7 @@ SELECT_ENTITIES = (
icon="mdi:spotlight-beam", icon="mdi:spotlight-beam",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="floodlight_mode", translation_key="floodlight_mode",
get_options=[mode.name for mode in SpotlightModeEnum], get_options=lambda api, ch: api.whiteled_mode_list(ch),
supported=lambda api, ch: api.supported(ch, "floodLight"), supported=lambda api, ch: api.supported(ch, "floodLight"),
value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name, value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name,
method=lambda api, ch, name: api.set_whiteled(ch, mode=name), method=lambda api, ch, name: api.set_whiteled(ch, mode=name),

View File

@@ -62,7 +62,9 @@
"state": { "state": {
"off": "Off", "off": "Off",
"auto": "Auto", "auto": "Auto",
"schedule": "Schedule" "schedule": "Schedule",
"adaptive": "Adaptive",
"autoadaptive": "Auto adaptive"
} }
}, },
"day_night_mode": { "day_night_mode": {

View File

@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioridwell"], "loggers": ["aioridwell"],
"requirements": ["aioridwell==2023.01.0"] "requirements": ["aioridwell==2023.07.0"]
} }

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock", "documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": ["python-roborock==0.29.2"] "requirements": ["python-roborock==0.30.0"]
} }

View File

@@ -90,6 +90,7 @@ class SlimProtoPlayer(MediaPlayerEntity):
| MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA
) )
_attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_name = None
def __init__(self, slimserver: SlimServer, player: SlimClient) -> None: def __init__(self, slimserver: SlimServer, player: SlimClient) -> None:
"""Initialize MediaPlayer entity.""" """Initialize MediaPlayer entity."""

View File

@@ -33,6 +33,7 @@ class StookwijzerSensor(SensorEntity):
_attr_attribution = "Data provided by stookwijzer.nu" _attr_attribution = "Data provided by stookwijzer.nu"
_attr_device_class = SensorDeviceClass.ENUM _attr_device_class = SensorDeviceClass.ENUM
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
_attr_translation_key = "stookwijzer" _attr_translation_key = "stookwijzer"
def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None:

View File

@@ -122,6 +122,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
| CoverEntityFeature.STOP_TILT | CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.SET_TILT_POSITION
) )
_attr_name = None
_attr_translation_key = "cover" _attr_translation_key = "cover"
CLOSED_UP_THRESHOLD = 80 CLOSED_UP_THRESHOLD = 80
CLOSED_DOWN_THRESHOLD = 20 CLOSED_DOWN_THRESHOLD = 20

View File

@@ -49,9 +49,10 @@ async def async_setup_entry(
class UpbLight(UpbAttachedEntity, LightEntity): class UpbLight(UpbAttachedEntity, LightEntity):
"""Representation of an UPB Light.""" """Representation of a UPB Light."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
def __init__(self, element, unique_id, upb): def __init__(self, element, unique_id, upb):
"""Initialize an UpbLight.""" """Initialize an UpbLight."""

View File

@@ -47,7 +47,7 @@ async def async_setup_entry(
class UpbLink(UpbEntity, Scene): class UpbLink(UpbEntity, Scene):
"""Representation of an UPB Link.""" """Representation of a UPB Link."""
def __init__(self, element, unique_id, upb): def __init__(self, element, unique_id, upb):
"""Initialize the base of all UPB devices.""" """Initialize the base of all UPB devices."""

View File

@@ -9,7 +9,7 @@
}, },
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pywemo"], "loggers": ["pywemo"],
"requirements": ["pywemo==0.9.1"], "requirements": ["pywemo==1.1.0"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Belkin International Inc." "manufacturer": "Belkin International Inc."

View File

@@ -43,6 +43,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
) )
_attr_name = None
def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None:
"""Initialize the Yale Alarm Device.""" """Initialize the Yale Alarm Device."""

View File

@@ -40,6 +40,8 @@ async def async_setup_entry(
class YaleDoorlock(YaleEntity, LockEntity): class YaleDoorlock(YaleEntity, LockEntity):
"""Representation of a Yale doorlock.""" """Representation of a Yale doorlock."""
_attr_name = None
def __init__( def __init__(
self, coordinator: YaleDataUpdateCoordinator, data: dict, code_format: int self, coordinator: YaleDataUpdateCoordinator, data: dict, code_format: int
) -> None: ) -> None:

View File

@@ -29,6 +29,7 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity):
"""A yale xs ble lock.""" """A yale xs ble lock."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
@callback @callback
def _async_update_state( def _async_update_state(

View File

@@ -25,10 +25,10 @@
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.101", "zha-quirks==0.0.101",
"zigpy-deconz==0.21.0", "zigpy-deconz==0.21.0",
"zigpy==0.56.1", "zigpy==0.56.2",
"zigpy-xbee==0.18.1", "zigpy-xbee==0.18.1",
"zigpy-zigate==0.11.0", "zigpy-zigate==0.11.0",
"zigpy-znp==0.11.2" "zigpy-znp==0.11.3"
], ],
"usb": [ "usb": [
{ {

View File

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

View File

@@ -16,7 +16,6 @@ from collections.abc import (
) )
import concurrent.futures import concurrent.futures
from contextlib import suppress from contextlib import suppress
from contextvars import ContextVar
import datetime import datetime
import enum import enum
import functools import functools
@@ -156,8 +155,6 @@ MAX_EXPECTED_ENTITY_IDS = 16384
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_cv_hass: ContextVar[HomeAssistant] = ContextVar("hass")
@functools.lru_cache(MAX_EXPECTED_ENTITY_IDS) @functools.lru_cache(MAX_EXPECTED_ENTITY_IDS)
def split_entity_id(entity_id: str) -> tuple[str, str]: def split_entity_id(entity_id: str) -> tuple[str, str]:
@@ -200,16 +197,27 @@ def is_callback(func: Callable[..., Any]) -> bool:
return getattr(func, "_hass_callback", False) is True return getattr(func, "_hass_callback", False) is True
class _Hass(threading.local):
"""Container which makes a HomeAssistant instance available to the event loop."""
hass: HomeAssistant | None = None
_hass = _Hass()
@callback @callback
def async_get_hass() -> HomeAssistant: def async_get_hass() -> HomeAssistant:
"""Return the HomeAssistant instance. """Return the HomeAssistant instance.
Raises LookupError if no HomeAssistant instance is available. Raises HomeAssistantError when called from the wrong thread.
This should be used where it's very cumbersome or downright impossible to pass This should be used where it's very cumbersome or downright impossible to pass
hass to the code which needs it. hass to the code which needs it.
""" """
return _cv_hass.get() if not _hass.hass:
raise HomeAssistantError("async_get_hass called from the wrong thread")
return _hass.hass
@enum.unique @enum.unique
@@ -293,9 +301,9 @@ class HomeAssistant:
config_entries: ConfigEntries = None # type: ignore[assignment] config_entries: ConfigEntries = None # type: ignore[assignment]
def __new__(cls) -> HomeAssistant: def __new__(cls) -> HomeAssistant:
"""Set the _cv_hass context variable.""" """Set the _hass thread local data."""
hass = super().__new__(cls) hass = super().__new__(cls)
_cv_hass.set(hass) _hass.hass = hass
return hass return hass
def __init__(self) -> None: def __init__(self) -> None:
@@ -1760,7 +1768,7 @@ class ServiceRegistry:
the context. Will return NONE if the service does not exist as there is the context. Will return NONE if the service does not exist as there is
other error handling when calling the service if it does not exist. other error handling when calling the service if it does not exist.
""" """
if not (handler := self._services[domain][service]): if not (handler := self._services[domain.lower()][service.lower()]):
return SupportsResponse.NONE return SupportsResponse.NONE
return handler.supports_response return handler.supports_response

View File

@@ -93,7 +93,7 @@ from homeassistant.core import (
split_entity_id, split_entity_id,
valid_entity_id, valid_entity_id,
) )
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.generated import currencies from homeassistant.generated import currencies
from homeassistant.generated.countries import COUNTRIES from homeassistant.generated.countries import COUNTRIES
from homeassistant.generated.languages import LANGUAGES from homeassistant.generated.languages import LANGUAGES
@@ -609,7 +609,7 @@ def template(value: Any | None) -> template_helper.Template:
raise vol.Invalid("template value should be a string") raise vol.Invalid("template value should be a string")
hass: HomeAssistant | None = None hass: HomeAssistant | None = None
with contextlib.suppress(LookupError): with contextlib.suppress(HomeAssistantError):
hass = async_get_hass() hass = async_get_hass()
template_value = template_helper.Template(str(value), hass) template_value = template_helper.Template(str(value), hass)
@@ -631,7 +631,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template:
raise vol.Invalid("template value does not contain a dynamic template") raise vol.Invalid("template value does not contain a dynamic template")
hass: HomeAssistant | None = None hass: HomeAssistant | None = None
with contextlib.suppress(LookupError): with contextlib.suppress(HomeAssistantError):
hass = async_get_hass() hass = async_get_hass()
template_value = template_helper.Template(str(value), hass) template_value = template_helper.Template(str(value), hass)
@@ -1098,7 +1098,7 @@ def _no_yaml_config_schema(
# pylint: disable-next=import-outside-toplevel # pylint: disable-next=import-outside-toplevel
from .issue_registry import IssueSeverity, async_create_issue from .issue_registry import IssueSeverity, async_create_issue
with contextlib.suppress(LookupError): with contextlib.suppress(HomeAssistantError):
hass = async_get_hass() hass = async_get_hass()
async_create_issue( async_create_issue(
hass, hass,

View File

@@ -670,6 +670,9 @@ def async_set_service_schema(
hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any] hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
) -> None: ) -> None:
"""Register a description for a service.""" """Register a description for a service."""
domain = domain.lower()
service = service.lower()
descriptions_cache: dict[ descriptions_cache: dict[
tuple[str, str], dict[str, Any] | None tuple[str, str], dict[str, Any] | None
] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})

View File

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

View File

@@ -124,7 +124,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.0.3 RachioPy==1.0.3
# homeassistant.components.python_script # homeassistant.components.python_script
RestrictedPython==6.0 RestrictedPython==6.1
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
RtmAPI==0.7.2 RtmAPI==0.7.2
@@ -252,7 +252,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.6.5 aiohomekit==2.6.7
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@@ -330,7 +330,7 @@ aioqsw==0.3.2
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.ridwell # homeassistant.components.ridwell
aioridwell==2023.01.0 aioridwell==2023.07.0
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0
@@ -565,7 +565,7 @@ brunt==1.2.0
bt-proximity==0.2.1 bt-proximity==0.2.1
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.12.0 bthome-ble==2.12.1
# homeassistant.components.bt_home_hub_5 # homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1 bthomehub5-devicelist==0.1.1
@@ -864,7 +864,7 @@ gitterpy==0.1.7
glances-api==0.4.3 glances-api==0.4.3
# homeassistant.components.goalzero # homeassistant.components.goalzero
goalzero==0.2.1 goalzero==0.2.2
# homeassistant.components.goodwe # homeassistant.components.goodwe
goodwe==0.2.31 goodwe==0.2.31
@@ -1615,7 +1615,7 @@ pycsspeechtts==1.0.8
# pycups==1.9.73 # pycups==1.9.73
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==2.9.0 pydaikin==2.10.5
# homeassistant.components.danfoss_air # homeassistant.components.danfoss_air
pydanfossair==0.1.0 pydanfossair==0.1.0
@@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.29.2 python-roborock==0.30.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@@ -2213,7 +2213,7 @@ pyvolumio==0.1.5
pywebpush==1.9.2 pywebpush==1.9.2
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==0.9.1 pywemo==1.1.0
# homeassistant.components.wilight # homeassistant.components.wilight
pywilight==0.0.74 pywilight==0.0.74
@@ -2267,7 +2267,7 @@ renault-api==0.1.13
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.1 reolink-aio==0.7.3
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@@ -2759,10 +2759,10 @@ zigpy-xbee==0.18.1
zigpy-zigate==0.11.0 zigpy-zigate==0.11.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.11.2 zigpy-znp==0.11.3
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.56.1 zigpy==0.56.2
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@@ -108,7 +108,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.0.3 RachioPy==1.0.3
# homeassistant.components.python_script # homeassistant.components.python_script
RestrictedPython==6.0 RestrictedPython==6.1
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
RtmAPI==0.7.2 RtmAPI==0.7.2
@@ -227,7 +227,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.6.5 aiohomekit==2.6.7
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@@ -302,7 +302,7 @@ aioqsw==0.3.2
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.ridwell # homeassistant.components.ridwell
aioridwell==2023.01.0 aioridwell==2023.07.0
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0
@@ -469,7 +469,7 @@ brottsplatskartan==0.0.1
brunt==1.2.0 brunt==1.2.0
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.12.0 bthome-ble==2.12.1
# homeassistant.components.buienradar # homeassistant.components.buienradar
buienradar==1.0.5 buienradar==1.0.5
@@ -677,7 +677,7 @@ gios==3.1.0
glances-api==0.4.3 glances-api==0.4.3
# homeassistant.components.goalzero # homeassistant.components.goalzero
goalzero==0.2.1 goalzero==0.2.2
# homeassistant.components.goodwe # homeassistant.components.goodwe
goodwe==0.2.31 goodwe==0.2.31
@@ -1197,7 +1197,7 @@ pycoolmasternet-async==0.1.5
pycsspeechtts==1.0.8 pycsspeechtts==1.0.8
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==2.9.0 pydaikin==2.10.5
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==113 pydeconz==113
@@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3 python-qbittorrent==0.4.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.29.2 python-roborock==0.30.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@@ -1621,7 +1621,7 @@ pyvolumio==0.1.5
pywebpush==1.9.2 pywebpush==1.9.2
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==0.9.1 pywemo==1.1.0
# homeassistant.components.wilight # homeassistant.components.wilight
pywilight==0.0.74 pywilight==0.0.74
@@ -1660,7 +1660,7 @@ renault-api==0.1.13
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.1 reolink-aio==0.7.3
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
@@ -2023,10 +2023,10 @@ zigpy-xbee==0.18.1
zigpy-zigate==0.11.0 zigpy-zigate==0.11.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.11.2 zigpy-znp==0.11.3
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.56.1 zigpy==0.56.2
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.49.0 zwave-js-server-python==0.49.0

View File

@@ -0,0 +1,128 @@
"""Define tests for the Daikin init."""
import asyncio
from unittest.mock import AsyncMock, PropertyMock, patch
from aiohttp import ClientConnectionError
import pytest
from homeassistant.components.daikin import update_unique_id
from homeassistant.components.daikin.const import DOMAIN, KEY_MAC
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .test_config_flow import HOST, MAC
from tests.common import MockConfigEntry
@pytest.fixture
def mock_daikin():
"""Mock pydaikin."""
async def mock_daikin_factory(*args, **kwargs):
"""Mock the init function in pydaikin."""
return Appliance
with patch("homeassistant.components.daikin.Appliance") as Appliance:
Appliance.factory.side_effect = mock_daikin_factory
type(Appliance).update_status = AsyncMock()
type(Appliance).inside_temperature = PropertyMock(return_value=22)
type(Appliance).target_temperature = PropertyMock(return_value=22)
type(Appliance).zones = PropertyMock(return_value=[("Zone 1", "0", 0)])
type(Appliance).fan_rate = PropertyMock(return_value=[])
type(Appliance).swing_modes = PropertyMock(return_value=[])
yield Appliance
DATA = {
"ver": "1_1_8",
"name": "DaikinAP00000",
"mac": MAC,
"model": "NOTSUPPORT",
}
INVALID_DATA = {**DATA, "name": None, "mac": HOST}
async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None:
"""Test unique id migration."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=HOST,
title=None,
data={CONF_HOST: HOST, KEY_MAC: HOST},
)
config_entry.add_to_hass(hass)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
type(mock_daikin).mac = PropertyMock(return_value=HOST)
type(mock_daikin).values = PropertyMock(return_value=INVALID_DATA)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.unique_id == HOST
assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None
entity = entity_registry.async_get("climate.daikin_127_0_0_1")
assert entity.unique_id == HOST
assert update_unique_id(entity, MAC) is not None
assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(HOST)
type(mock_daikin).mac = PropertyMock(return_value=MAC)
type(mock_daikin).values = PropertyMock(return_value=DATA)
assert config_entry.unique_id != MAC
assert await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.unique_id == MAC
assert (
device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000"
)
entity = entity_registry.async_get("climate.daikin_127_0_0_1")
assert entity.unique_id == MAC
assert update_unique_id(entity, MAC) is None
assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC)
async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None:
"""Test unique id migration."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=MAC,
data={CONF_HOST: HOST, KEY_MAC: MAC},
)
config_entry.add_to_hass(hass)
mock_daikin.factory.side_effect = ClientConnectionError
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None:
"""Test unique id migration."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=MAC,
data={CONF_HOST: HOST, KEY_MAC: MAC},
)
config_entry.add_to_hass(hass)
mock_daikin.factory.side_effect = asyncio.TimeoutError
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_RETRY

View File

@@ -1308,3 +1308,45 @@ async def test_option_flow(
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value} assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value}
assert len(mock_reload.mock_calls) == int(option_value) assert len(mock_reload.mock_calls) == int(option_value)
async def test_user_discovers_name_no_dashboard(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_setup_entry: None,
) -> None:
"""Test user step can discover the name and the there is not dashboard."""
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError("Wrong key", "test"),
DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
),
]
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK

View File

@@ -58,7 +58,9 @@ async def test_setup_dashboard_fails(
assert mock_config_entry.state == ConfigEntryState.LOADED assert mock_config_entry.state == ConfigEntryState.LOADED
assert mock_get_devices.call_count == 1 assert mock_get_devices.call_count == 1
assert dashboard.STORAGE_KEY not in hass_storage # The dashboard addon might recover later so we still
# allow it to be set up.
assert dashboard.STORAGE_KEY in hass_storage
async def test_setup_dashboard_fails_when_already_setup( async def test_setup_dashboard_fails_when_already_setup(

View File

@@ -8,6 +8,9 @@
# name: test_image_entity[fc_data0] # name: test_image_entity[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82' b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82'
# --- # ---
# name: test_image_entity[fc_data1]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82'
# ---
# name: test_image_update[fc_data0] # name: test_image_update[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf9IDATx\xda\xedV\xc1\r\xc40\x0cB\xb7\x80\xf7\xdf\x92\r\\\xb0\xfb\xeb\xe7\xaa\xf0l\xd4\xaaQ\x1e\xc8\x06L\x8a~,\xe2;{s\x06\xa0\xd8z9\xdb\xe6\x0f\xcf\xf5\xef\x99\xf0J\x0f\x85\x86*o\xcf\xf1\x04\x04\x1ak\xb6\x11<\x97\xa6\xa6\x83x&\xb32x\x86\xa4\xab\xeb\x08\x7f\x16\xf5^\x11}\xbd$\xb0\x80k=t\xcc\x9f\xfdg\xfa\xda\xe5\x1d\xe3\t\x8br_\xdb3\x85D}\x063u\x00\x03\xfd\xb6<\xe2\xeaL\xa2y<\xae\xcf\xe3!\x895\xbfL\xf07\x0eT]n7\xc3_{0\xd4\xefx:\xc0\x1f\xc6}\x9e\xb7\x84\x1e\xfb\x91\x0e\x12\x84\t=z\xd2t\x07\x8e\x1d\xc9\x03\xc7\xa9G\xb7\x12\xf3&0\x176\x19\x98\xc8g\x8b;\x88@\xc6\x7f\x93\xa9\xfbVD\xdf\x193\xde9\x1d\xd1\xc3\x9ev`E\xf2oo\xa3\xe1/\x847\xad\x8a?0t\xffN\xb4p\xf35\xf3\x7f\x80\xad\xafS\xf7\x1bD`D\x8f\xef\x9f\xf0\xe0\xec\x02\xa4\xc0\x83\x92\xcf\xf3\xf9a\x00\x00\x00\x00IEND\xaeB`\x82' b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf9IDATx\xda\xedV\xc1\r\xc40\x0cB\xb7\x80\xf7\xdf\x92\r\\\xb0\xfb\xeb\xe7\xaa\xf0l\xd4\xaaQ\x1e\xc8\x06L\x8a~,\xe2;{s\x06\xa0\xd8z9\xdb\xe6\x0f\xcf\xf5\xef\x99\xf0J\x0f\x85\x86*o\xcf\xf1\x04\x04\x1ak\xb6\x11<\x97\xa6\xa6\x83x&\xb32x\x86\xa4\xab\xeb\x08\x7f\x16\xf5^\x11}\xbd$\xb0\x80k=t\xcc\x9f\xfdg\xfa\xda\xe5\x1d\xe3\t\x8br_\xdb3\x85D}\x063u\x00\x03\xfd\xb6<\xe2\xeaL\xa2y<\xae\xcf\xe3!\x895\xbfL\xf07\x0eT]n7\xc3_{0\xd4\xefx:\xc0\x1f\xc6}\x9e\xb7\x84\x1e\xfb\x91\x0e\x12\x84\t=z\xd2t\x07\x8e\x1d\xc9\x03\xc7\xa9G\xb7\x12\xf3&0\x176\x19\x98\xc8g\x8b;\x88@\xc6\x7f\x93\xa9\xfbVD\xdf\x193\xde9\x1d\xd1\xc3\x9ev`E\xf2oo\xa3\xe1/\x847\xad\x8a?0t\xffN\xb4p\xf35\xf3\x7f\x80\xad\xafS\xf7\x1bD`D\x8f\xef\x9f\xf0\xe0\xec\x02\xa4\xc0\x83\x92\xcf\xf3\xf9a\x00\x00\x00\x00IEND\xaeB`\x82'
# --- # ---

View File

@@ -60,11 +60,31 @@ GUEST_WIFI_CHANGED: dict[str, dict] = {
GUEST_WIFI_DISABLED: dict[str, dict] = { GUEST_WIFI_DISABLED: dict[str, dict] = {
"WLANConfiguration0": {}, "WLANConfiguration0": {},
"WLANConfiguration1": {"GetInfo": {"NewEnable": False}}, "WLANConfiguration1": {
"GetInfo": {
"NewEnable": False,
"NewStatus": "Up",
"NewSSID": "GuestWifi",
"NewBeaconType": "11iandWPA3",
"NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3",
"NewStandard": "ax",
"NewBSSID": "1C:ED:6F:12:34:13",
},
"GetSSID": {
"NewSSID": "GuestWifi",
},
"GetSecurityKeys": {"NewKeyPassphrase": "1234567890"},
},
} }
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})]) @pytest.mark.parametrize(
("fc_data"),
[
({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED}),
({**MOCK_FB_SERVICES, **GUEST_WIFI_DISABLED}),
],
)
async def test_image_entity( async def test_image_entity(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
@@ -150,23 +170,3 @@ async def test_image_update(
assert resp_body != resp_body_new assert resp_body != resp_body_new
assert resp_body_new == snapshot assert resp_body_new == snapshot
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_DISABLED})])
async def test_image_guest_wifi_disabled(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
fc_class_mock,
fh_class_mock,
) -> None:
"""Test image entities."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
images = hass.states.async_all(IMAGE_DOMAIN)
assert len(images) == 0

View File

@@ -1 +1,174 @@
"""Tests for the myStrom integration.""" """Tests for the myStrom integration."""
from typing import Any, Optional
def get_default_device_response(device_type: int | None) -> dict[str, Any]:
"""Return default device response."""
response = {
"version": "2.59.32",
"mac": "6001940376EB",
"ssid": "personal",
"ip": "192.168.0.23",
"mask": "255.255.255.0",
"gw": "192.168.0.1",
"dns": "192.168.0.1",
"static": False,
"connected": True,
"signal": 94,
}
if device_type is not None:
response["type"] = device_type
return response
def get_default_bulb_state() -> dict[str, Any]:
"""Get default bulb state."""
return {
"type": "rgblamp",
"battery": False,
"reachable": True,
"meshroot": True,
"on": False,
"color": "46;18;100",
"mode": "hsv",
"ramp": 10,
"power": 0.45,
"fw_version": "2.58.0",
}
def get_default_switch_state() -> dict[str, Any]:
"""Get default switch state."""
return {
"power": 1.69,
"Ws": 0.81,
"relay": True,
"temperature": 24.87,
"version": "2.59.32",
"mac": "6001940376EB",
"ssid": "personal",
"ip": "192.168.0.23",
"mask": "255.255.255.0",
"gw": "192.168.0.1",
"dns": "192.168.0.1",
"static": False,
"connected": True,
"signal": 94,
}
class MyStromDeviceMock:
"""Base device mock."""
def __init__(self, state: dict[str, Any]) -> None:
"""Initialize device mock."""
self._requested_state = False
self._state = state
async def get_state(self) -> None:
"""Set if state is requested."""
self._requested_state = True
class MyStromBulbMock(MyStromDeviceMock):
"""MyStrom Bulb mock."""
def __init__(self, mac: str, state: dict[str, Any]) -> None:
"""Initialize bulb mock."""
super().__init__(state)
self.mac = mac
@property
def firmware(self) -> Optional[str]:
"""Return current firmware."""
if not self._requested_state:
return None
return self._state["fw_version"]
@property
def consumption(self) -> Optional[float]:
"""Return current firmware."""
if not self._requested_state:
return None
return self._state["power"]
@property
def color(self) -> Optional[str]:
"""Return current color settings."""
if not self._requested_state:
return None
return self._state["color"]
@property
def mode(self) -> Optional[str]:
"""Return current mode."""
if not self._requested_state:
return None
return self._state["mode"]
@property
def transition_time(self) -> Optional[int]:
"""Return current transition time (ramp)."""
if not self._requested_state:
return None
return self._state["ramp"]
@property
def bulb_type(self) -> Optional[str]:
"""Return the type of the bulb."""
if not self._requested_state:
return None
return self._state["type"]
@property
def state(self) -> Optional[bool]:
"""Return the current state of the bulb."""
if not self._requested_state:
return None
return self._state["on"]
class MyStromSwitchMock(MyStromDeviceMock):
"""MyStrom Switch mock."""
@property
def relay(self) -> Optional[bool]:
"""Return the relay state."""
if not self._requested_state:
return None
return self._state["on"]
@property
def consumption(self) -> Optional[float]:
"""Return the current power consumption in mWh."""
if not self._requested_state:
return None
return self._state["power"]
@property
def consumedWs(self) -> Optional[float]:
"""The average of energy consumed per second since last report call."""
if not self._requested_state:
return None
return self._state["Ws"]
@property
def firmware(self) -> Optional[str]:
"""Return the current firmware."""
if not self._requested_state:
return None
return self._state["version"]
@property
def mac(self) -> Optional[str]:
"""Return the MAC address."""
if not self._requested_state:
return None
return self._state["mac"]
@property
def temperature(self) -> Optional[float]:
"""Return the current temperature in celsius."""
if not self._requested_state:
return None
return self._state["temperature"]

View File

@@ -2,11 +2,19 @@
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, PropertyMock, patch
from pymystrom.exceptions import MyStromConnectionError from pymystrom.exceptions import MyStromConnectionError
import pytest
from homeassistant.components.mystrom.const import DOMAIN from homeassistant.components.mystrom.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import (
MyStromBulbMock,
MyStromSwitchMock,
get_default_bulb_state,
get_default_device_response,
get_default_switch_state,
)
from .conftest import DEVICE_MAC from .conftest import DEVICE_MAC
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@@ -16,36 +24,27 @@ async def init_integration(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
device_type: int, device_type: int,
bulb_type: str = "strip",
) -> None: ) -> None:
"""Inititialize integration for testing.""" """Inititialize integration for testing."""
with patch( with patch(
"pymystrom.get_device_info", "pymystrom.get_device_info",
side_effect=AsyncMock(return_value={"type": device_type, "mac": DEVICE_MAC}), side_effect=AsyncMock(return_value=get_default_device_response(device_type)),
), patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), patch(
"pymystrom.bulb.MyStromBulb.get_state", return_value={}
), patch( ), patch(
"pymystrom.bulb.MyStromBulb.bulb_type", bulb_type "homeassistant.components.mystrom._get_mystrom_bulb",
return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()),
), patch( ), patch(
"pymystrom.switch.MyStromSwitch.mac", "homeassistant.components.mystrom._get_mystrom_switch",
new_callable=PropertyMock, return_value=MyStromSwitchMock(get_default_switch_state()),
return_value=DEVICE_MAC,
), patch(
"pymystrom.bulb.MyStromBulb.mac",
new_callable=PropertyMock,
return_value=DEVICE_MAC,
): ):
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.LOADED
async def test_init_switch_and_unload( async def test_init_switch_and_unload(
hass: HomeAssistant, config_entry: MockConfigEntry hass: HomeAssistant, config_entry: MockConfigEntry
) -> None: ) -> None:
"""Test the initialization of a myStrom switch.""" """Test the initialization of a myStrom switch."""
await init_integration(hass, config_entry, 101) await init_integration(hass, config_entry, 106)
state = hass.states.get("switch.mystrom_device") state = hass.states.get("switch.mystrom_device")
assert state is not None assert state is not None
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
@@ -56,12 +55,35 @@ async def test_init_switch_and_unload(
assert not hass.data.get(DOMAIN) assert not hass.data.get(DOMAIN)
async def test_init_bulb(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: @pytest.mark.parametrize(
("device_type", "platform", "entry_state", "entity_state_none"),
[
(None, "switch", ConfigEntryState.LOADED, False),
(102, "light", ConfigEntryState.LOADED, False),
(103, "button", ConfigEntryState.SETUP_ERROR, True),
(104, "button", ConfigEntryState.SETUP_ERROR, True),
(105, "light", ConfigEntryState.LOADED, False),
(106, "switch", ConfigEntryState.LOADED, False),
(107, "switch", ConfigEntryState.LOADED, False),
(110, "sensor", ConfigEntryState.SETUP_ERROR, True),
(113, "switch", ConfigEntryState.SETUP_ERROR, True),
(118, "button", ConfigEntryState.SETUP_ERROR, True),
(120, "switch", ConfigEntryState.SETUP_ERROR, True),
],
)
async def test_init_bulb(
hass: HomeAssistant,
config_entry: MockConfigEntry,
device_type: int,
platform: str,
entry_state: ConfigEntryState,
entity_state_none: bool,
) -> None:
"""Test the initialization of a myStrom bulb.""" """Test the initialization of a myStrom bulb."""
await init_integration(hass, config_entry, 102) await init_integration(hass, config_entry, device_type)
state = hass.states.get("light.mystrom_device") state = hass.states.get(f"{platform}.mystrom_device")
assert state is not None assert (state is None) == entity_state_none
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is entry_state
async def test_init_of_unknown_bulb( async def test_init_of_unknown_bulb(
@@ -120,7 +142,7 @@ async def test_init_cannot_connect_because_of_get_state(
"""Test error handling for failing get_state.""" """Test error handling for failing get_state."""
with patch( with patch(
"pymystrom.get_device_info", "pymystrom.get_device_info",
side_effect=AsyncMock(return_value={"type": 101, "mac": DEVICE_MAC}), side_effect=AsyncMock(return_value=get_default_device_response(101)),
), patch( ), patch(
"pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError() "pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError()
), patch( ), patch(
@@ -129,4 +151,4 @@ async def test_init_cannot_connect_because_of_get_state(
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_ERROR assert config_entry.state == ConfigEntryState.SETUP_RETRY

View File

@@ -35,6 +35,8 @@ SERIAL_NUMBER = 0x12635436566
# Get serial number Command 0x85. Serial is 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566
SERIAL_RESPONSE = "850000012635436566" SERIAL_RESPONSE = "850000012635436566"
# Model and version command 0x82
MODEL_AND_VERSION_RESPONSE = "820006090C"
# Get available stations command 0x83 # Get available stations command 0x83
AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones
EMPTY_STATIONS_RESPONSE = "830000000000" EMPTY_STATIONS_RESPONSE = "830000000000"
@@ -183,7 +185,13 @@ def mock_api_responses(
These are returned in the order they are requested by the update coordinator. These are returned in the order they are requested by the update coordinator.
""" """
return [stations_response, zone_state_response, rain_response, rain_delay_response] return [
MODEL_AND_VERSION_RESPONSE,
stations_response,
zone_state_response,
rain_response,
rain_delay_response,
]
@pytest.fixture(name="responses") @pytest.fixture(name="responses")

View File

@@ -70,6 +70,8 @@ async def test_set_value(
device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)})
assert device assert device
assert device.name == "Rain Bird Controller" assert device.name == "Rain Bird Controller"
assert device.model == "ST8x-WiFi"
assert device.sw_version == "9.12"
aioclient_mock.mock_calls.clear() aioclient_mock.mock_calls.clear()
responses.append(mock_response(ACK_ECHO)) responses.append(mock_response(ACK_ECHO))

View File

@@ -1,10 +1,11 @@
"""Test zha siren.""" """Test zha siren."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import ANY, call, patch
import pytest import pytest
from zigpy.const import SIG_EP_PROFILE from zigpy.const import SIG_EP_PROFILE
import zigpy.profiles.zha as zha import zigpy.profiles.zha as zha
import zigpy.zcl
import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.security as security import zigpy.zcl.clusters.security as security
import zigpy.zcl.foundation as zcl_f import zigpy.zcl.foundation as zcl_f
@@ -85,48 +86,76 @@ async def test_siren(hass: HomeAssistant, siren) -> None:
# turn on from HA # turn on from HA
with patch( with patch(
"zigpy.zcl.Cluster.request", "zigpy.device.Device.request",
return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
), patch(
"zigpy.zcl.Cluster.request",
side_effect=zigpy.zcl.Cluster.request,
autospec=True,
): ):
# turn on via UI # turn on via UI
await hass.services.async_call( await hass.services.async_call(
SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
) )
assert len(cluster.request.mock_calls) == 1 assert cluster.request.mock_calls == [
assert cluster.request.call_args[0][0] is False call(
assert cluster.request.call_args[0][1] == 0 cluster,
assert cluster.request.call_args[0][3] == 50 # bitmask for default args False,
assert cluster.request.call_args[0][4] == 5 # duration in seconds 0,
assert cluster.request.call_args[0][5] == 0 ANY,
assert cluster.request.call_args[0][6] == 2 50, # bitmask for default args
5, # duration in seconds
0,
2,
manufacturer=None,
expect_reply=True,
tsn=None,
)
]
# test that the state has changed to on # test that the state has changed to on
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# turn off from HA # turn off from HA
with patch( with patch(
"zigpy.zcl.Cluster.request", "zigpy.device.Device.request",
return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]),
), patch(
"zigpy.zcl.Cluster.request",
side_effect=zigpy.zcl.Cluster.request,
autospec=True,
): ):
# turn off via UI # turn off via UI
await hass.services.async_call( await hass.services.async_call(
SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
) )
assert len(cluster.request.mock_calls) == 1 assert cluster.request.mock_calls == [
assert cluster.request.call_args[0][0] is False call(
assert cluster.request.call_args[0][1] == 0 cluster,
assert cluster.request.call_args[0][3] == 2 # bitmask for default args False,
assert cluster.request.call_args[0][4] == 5 # duration in seconds 0,
assert cluster.request.call_args[0][5] == 0 ANY,
assert cluster.request.call_args[0][6] == 2 2, # bitmask for default args
5, # duration in seconds
0,
2,
manufacturer=None,
expect_reply=True,
tsn=None,
)
]
# test that the state has changed to off # test that the state has changed to off
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA # turn on from HA
with patch( with patch(
"zigpy.zcl.Cluster.request", "zigpy.device.Device.request",
return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]),
), patch(
"zigpy.zcl.Cluster.request",
side_effect=zigpy.zcl.Cluster.request,
autospec=True,
): ):
# turn on via UI # turn on via UI
await hass.services.async_call( await hass.services.async_call(
@@ -140,14 +169,21 @@ async def test_siren(hass: HomeAssistant, siren) -> None:
}, },
blocking=True, blocking=True,
) )
assert len(cluster.request.mock_calls) == 1 assert cluster.request.mock_calls == [
assert cluster.request.call_args[0][0] is False call(
assert cluster.request.call_args[0][1] == 0 cluster,
assert cluster.request.call_args[0][3] == 97 # bitmask for passed args False,
assert cluster.request.call_args[0][4] == 10 # duration in seconds 0,
assert cluster.request.call_args[0][5] == 0 ANY,
assert cluster.request.call_args[0][6] == 2 97, # bitmask for passed args
10, # duration in seconds
0,
2,
manufacturer=None,
expect_reply=True,
tsn=None,
)
]
# test that the state has changed to on # test that the state has changed to on
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON

View File

@@ -490,17 +490,7 @@ def hass_fixture_setup() -> list[bool]:
@pytest.fixture @pytest.fixture
def hass(_hass: HomeAssistant) -> HomeAssistant: async def hass(
"""Fixture to provide a test instance of Home Assistant."""
# This wraps the async _hass fixture inside a sync fixture, to ensure
# the `hass` context variable is set in the execution context in which
# the test itself is executed
ha._cv_hass.set(_hass)
return _hass
@pytest.fixture
async def _hass(
hass_fixture_setup: list[bool], hass_fixture_setup: list[bool],
event_loop: asyncio.AbstractEventLoop, event_loop: asyncio.AbstractEventLoop,
load_registries: bool, load_registries: bool,

View File

@@ -12,6 +12,7 @@ import voluptuous as vol
import homeassistant import homeassistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import ( from homeassistant.helpers import (
config_validation as cv, config_validation as cv,
issue_registry as ir, issue_registry as ir,
@@ -383,7 +384,7 @@ def test_service() -> None:
schema("homeassistant.turn_on") schema("homeassistant.turn_on")
def test_service_schema() -> None: def test_service_schema(hass: HomeAssistant) -> None:
"""Test service_schema validation.""" """Test service_schema validation."""
options = ( options = (
{}, {},
@@ -1550,10 +1551,10 @@ def test_config_entry_only_schema_cant_find_module() -> None:
def test_config_entry_only_schema_no_hass( def test_config_entry_only_schema_no_hass(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test if the the hass context var is not set in our context.""" """Test if the the hass context is not set in our context."""
with patch( with patch(
"homeassistant.helpers.config_validation.async_get_hass", "homeassistant.helpers.config_validation.async_get_hass",
side_effect=LookupError, side_effect=HomeAssistantError,
): ):
cv.config_entry_only_config_schema("test_domain")( cv.config_entry_only_config_schema("test_domain")(
{"test_domain": {"foo": "bar"}} {"test_domain": {"foo": "bar"}}

View File

@@ -724,6 +724,27 @@ async def test_async_get_all_descriptions_dynamically_created_services(
} }
async def test_register_with_mixed_case(hass: HomeAssistant) -> None:
"""Test registering a service with mixed case.
For backwards compatibility, we have historically allowed mixed case,
and automatically converted it to lowercase.
"""
logger = hass.components.logger
logger_config = {logger.DOMAIN: {}}
await async_setup_component(hass, logger.DOMAIN, logger_config)
logger_domain_mixed = "LoGgEr"
hass.services.async_register(
logger_domain_mixed, "NeW_SeRVICE", lambda x: None, None
)
service.async_set_service_schema(
hass, logger_domain_mixed, "NeW_SeRVICE", {"description": "new service"}
)
descriptions = await service.async_get_all_descriptions(hass)
assert "description" in descriptions[logger.DOMAIN]["new_service"]
assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service"
async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None: async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None:
"""Test service calls invoked only if entity has required features.""" """Test service calls invoked only if entity has required features."""
test_service_mock = AsyncMock(return_value=None) test_service_mock = AsyncMock(return_value=None)

View File

@@ -9,10 +9,12 @@ import gc
import logging import logging
import os import os
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import threading
import time import time
from typing import Any from typing import Any
from unittest.mock import MagicMock, Mock, PropertyMock, patch from unittest.mock import MagicMock, Mock, PropertyMock, patch
import async_timeout
import pytest import pytest
import voluptuous as vol import voluptuous as vol
@@ -40,6 +42,7 @@ from homeassistant.core import (
ServiceResponse, ServiceResponse,
State, State,
SupportsResponse, SupportsResponse,
callback,
) )
from homeassistant.exceptions import ( from homeassistant.exceptions import (
HomeAssistantError, HomeAssistantError,
@@ -202,6 +205,184 @@ def test_async_run_hass_job_delegates_non_async() -> None:
assert len(hass.async_add_hass_job.mock_calls) == 1 assert len(hass.async_add_hass_job.mock_calls) == 1
async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None:
"""Test calling async_get_hass via different paths.
The test asserts async_get_hass can be called from:
- Coroutines and callbacks
- Callbacks scheduled from callbacks, coroutines and threads
- Coroutines scheduled from callbacks, coroutines and threads
The test also asserts async_get_hass can not be called from threads
other than the event loop.
"""
task_finished = asyncio.Event()
def can_call_async_get_hass() -> bool:
"""Test if it's possible to call async_get_hass."""
try:
if ha.async_get_hass() is hass:
return True
raise Exception
except HomeAssistantError:
return False
raise Exception
# Test scheduling a coroutine which calls async_get_hass via hass.async_create_task
async def _async_create_task() -> None:
task_finished.set()
assert can_call_async_get_hass()
hass.async_create_task(_async_create_task(), "create_task")
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a callback which calls async_get_hass via hass.async_add_job
@callback
def _add_job() -> None:
assert can_call_async_get_hass()
task_finished.set()
hass.async_add_job(_add_job)
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a callback which calls async_get_hass from a callback
@callback
def _schedule_callback_from_callback() -> None:
@callback
def _callback():
assert can_call_async_get_hass()
task_finished.set()
# Test the scheduled callback itself can call async_get_hass
assert can_call_async_get_hass()
hass.async_add_job(_callback)
_schedule_callback_from_callback()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a coroutine which calls async_get_hass from a callback
@callback
def _schedule_coroutine_from_callback() -> None:
async def _coroutine():
assert can_call_async_get_hass()
task_finished.set()
# Test the scheduled callback itself can call async_get_hass
assert can_call_async_get_hass()
hass.async_add_job(_coroutine())
_schedule_coroutine_from_callback()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a callback which calls async_get_hass from a coroutine
async def _schedule_callback_from_coroutine() -> None:
@callback
def _callback():
assert can_call_async_get_hass()
task_finished.set()
# Test the coroutine itself can call async_get_hass
assert can_call_async_get_hass()
hass.async_add_job(_callback)
await _schedule_callback_from_coroutine()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a coroutine which calls async_get_hass from a coroutine
async def _schedule_callback_from_coroutine() -> None:
async def _coroutine():
assert can_call_async_get_hass()
task_finished.set()
# Test the coroutine itself can call async_get_hass
assert can_call_async_get_hass()
await hass.async_create_task(_coroutine())
await _schedule_callback_from_coroutine()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a callback which calls async_get_hass from an executor
def _async_add_executor_job_add_job() -> None:
@callback
def _async_add_job():
assert can_call_async_get_hass()
task_finished.set()
# Test the executor itself can not call async_get_hass
assert not can_call_async_get_hass()
hass.add_job(_async_add_job)
await hass.async_add_executor_job(_async_add_executor_job_add_job)
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a coroutine which calls async_get_hass from an executor
def _async_add_executor_job_create_task() -> None:
async def _async_create_task() -> None:
assert can_call_async_get_hass()
task_finished.set()
# Test the executor itself can not call async_get_hass
assert not can_call_async_get_hass()
hass.create_task(_async_create_task())
await hass.async_add_executor_job(_async_add_executor_job_create_task)
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
# Test scheduling a callback which calls async_get_hass from a worker thread
class MyJobAddJob(threading.Thread):
@callback
def _my_threaded_job_add_job(self) -> None:
assert can_call_async_get_hass()
task_finished.set()
def run(self) -> None:
# Test the worker thread itself can not call async_get_hass
assert not can_call_async_get_hass()
hass.add_job(self._my_threaded_job_add_job)
my_job_add_job = MyJobAddJob()
my_job_add_job.start()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
my_job_add_job.join()
# Test scheduling a coroutine which calls async_get_hass from a worker thread
class MyJobCreateTask(threading.Thread):
async def _my_threaded_job_create_task(self) -> None:
assert can_call_async_get_hass()
task_finished.set()
def run(self) -> None:
# Test the worker thread itself can not call async_get_hass
assert not can_call_async_get_hass()
hass.create_task(self._my_threaded_job_create_task())
my_job_create_task = MyJobCreateTask()
my_job_create_task.start()
async with async_timeout.timeout(1):
await task_finished.wait()
task_finished.clear()
my_job_create_task.join()
async def test_stage_shutdown(hass: HomeAssistant) -> None: async def test_stage_shutdown(hass: HomeAssistant) -> None:
"""Simulate a shutdown, test calling stuff.""" """Simulate a shutdown, test calling stuff."""
test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP)