mirror of
https://github.com/home-assistant/core.git
synced 2025-08-06 14:15:12 +02:00
2023.7.2 (#96487)
This commit is contained in:
@@ -182,7 +182,6 @@ omit =
|
||||
homeassistant/components/crownstone/listeners.py
|
||||
homeassistant/components/cups/sensor.py
|
||||
homeassistant/components/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/__init__.py
|
||||
homeassistant/components/daikin/climate.py
|
||||
homeassistant/components/daikin/sensor.py
|
||||
homeassistant/components/daikin/switch.py
|
||||
|
@@ -42,6 +42,7 @@ class BlinkSyncModule(AlarmControlPanelEntity):
|
||||
_attr_icon = ICON
|
||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data, name, sync):
|
||||
"""Initialize the alarm control panel."""
|
||||
|
@@ -58,13 +58,14 @@ async def async_setup_entry(
|
||||
class BlinkBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a Blink binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, data, camera, description: BinarySensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{DOMAIN} {camera} {description.name}"
|
||||
self._camera = data.cameras[camera]
|
||||
self._attr_unique_id = f"{self._camera.serial}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
@@ -38,6 +38,7 @@ async def async_setup_entry(
|
||||
class BlinkCamera(Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, data, name, camera):
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==2.12.0"]
|
||||
"requirements": ["bthome-ble==2.12.1"]
|
||||
}
|
||||
|
@@ -301,55 +301,55 @@
|
||||
"name": "Condition 1d",
|
||||
"state": {
|
||||
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]"
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"condition_2d": {
|
||||
"name": "Condition 2d",
|
||||
"state": {
|
||||
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]"
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"condition_3d": {
|
||||
"name": "Condition 3d",
|
||||
"state": {
|
||||
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]"
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"condition_4d": {
|
||||
"name": "Condition 4d",
|
||||
"state": {
|
||||
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]"
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"condition_5d": {
|
||||
"name": "Condition 5d",
|
||||
"state": {
|
||||
"clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]"
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"conditioncode_1d": {
|
||||
@@ -371,76 +371,76 @@
|
||||
"name": "Detailed condition 1d",
|
||||
"state": {
|
||||
"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-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
|
||||
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]",
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
|
||||
"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-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
|
||||
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]"
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"conditiondetailed_2d": {
|
||||
"name": "Detailed condition 2d",
|
||||
"state": {
|
||||
"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-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
|
||||
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]",
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
|
||||
"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-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
|
||||
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]"
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"conditiondetailed_3d": {
|
||||
"name": "Detailed condition 3d",
|
||||
"state": {
|
||||
"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-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
|
||||
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]",
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
|
||||
"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-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
|
||||
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]"
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"conditiondetailed_4d": {
|
||||
"name": "Detailed condition 4d",
|
||||
"state": {
|
||||
"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-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
|
||||
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]",
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
|
||||
"light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]",
|
||||
"partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]",
|
||||
@@ -455,21 +455,21 @@
|
||||
"name": "Detailed condition 5d",
|
||||
"state": {
|
||||
"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-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]",
|
||||
"partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]",
|
||||
"cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]",
|
||||
"fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]",
|
||||
"rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]",
|
||||
"cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]",
|
||||
"fog": "[%key:component::weather::entity_component::_::state::fog%]",
|
||||
"rainy": "[%key:component::weather::entity_component::_::state::rainy%]",
|
||||
"light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]",
|
||||
"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-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]",
|
||||
"partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]",
|
||||
"snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]"
|
||||
"snowy": "[%key:component::weather::entity_component::_::state::snowy%]",
|
||||
"snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]",
|
||||
"lightning": "[%key:component::weather::entity_component::_::state::lightning%]"
|
||||
}
|
||||
},
|
||||
"conditionexact_1d": {
|
||||
|
@@ -15,8 +15,9 @@ from homeassistant.const import (
|
||||
CONF_UUID,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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:
|
||||
return False
|
||||
|
||||
await async_migrate_unique_id(hass, entry, daikin_api)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
@@ -67,7 +70,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
@@ -127,3 +130,82 @@ class DaikinApi:
|
||||
name=info.get("name"),
|
||||
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}
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pydaikin==2.9.0"],
|
||||
"requirements": ["pydaikin==2.10.5"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ async def async_setup_entry(
|
||||
[
|
||||
DaikinZoneSwitch(daikin_api, zone_id)
|
||||
for zone_id, zone in enumerate(zones)
|
||||
if zone != ("-", "0")
|
||||
if zone[0] != "-"
|
||||
]
|
||||
)
|
||||
if daikin_api.device.support_advanced_modes:
|
||||
|
@@ -60,7 +60,6 @@ class ServiceDetails(NamedTuple):
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"),
|
||||
"yamaha": ServiceDetails("media_player", "yamaha"),
|
||||
"openhome": ServiceDetails("media_player", "openhome"),
|
||||
"bluesound": ServiceDetails("media_player", "bluesound"),
|
||||
}
|
||||
|
||||
@@ -87,6 +86,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
||||
SERVICE_MOBILE_APP,
|
||||
SERVICE_NETGEAR,
|
||||
SERVICE_OCTOPRINT,
|
||||
"openhome",
|
||||
"philips_hue",
|
||||
SERVICE_SAMSUNG_PRINTER,
|
||||
"sonos",
|
||||
|
@@ -76,6 +76,7 @@ class ControllerEntity(ClimateEntity):
|
||||
|
||||
_attr_fan_modes = list(_HA_FAN_TO_ESCEA)
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_icon = ICON
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
|
@@ -388,6 +388,7 @@ async def async_setup_entry( # noqa: C901
|
||||
assert cli.api_version is not None
|
||||
entry_data.api_version = cli.api_version
|
||||
entry_data.available = True
|
||||
entry_data.expected_disconnect = True
|
||||
if entry_data.device_info.name:
|
||||
reconnect_logic.name = entry_data.device_info.name
|
||||
|
||||
|
@@ -34,7 +34,7 @@ from .const import (
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
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_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
||||
@@ -391,7 +391,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
if (
|
||||
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
|
||||
|
||||
|
@@ -93,13 +93,6 @@ class ESPHomeDashboardManager:
|
||||
hass, addon_slug, url, async_get_clientsession(hass)
|
||||
)
|
||||
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
|
||||
|
||||
@@ -143,7 +136,14 @@ class ESPHomeDashboardManager:
|
||||
|
||||
@callback
|
||||
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)
|
||||
return manager.async_get() if manager else None
|
||||
|
||||
|
@@ -30,9 +30,6 @@ async def async_setup_entry(
|
||||
avm_wrapper.fritz_guest_wifi.get_info
|
||||
)
|
||||
|
||||
if not guest_wifi_info.get("NewEnable"):
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
FritzGuestWifiQRImage(
|
||||
|
@@ -16,5 +16,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goalzero"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["goalzero==0.2.1"]
|
||||
"requirements": ["goalzero==0.2.2"]
|
||||
}
|
||||
|
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==2.6.5"],
|
||||
"requirements": ["aiohomekit==2.6.7"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity):
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE}
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_features = LightEntityFeature.EFFECT
|
||||
|
||||
def __init__(
|
||||
|
@@ -22,6 +22,24 @@ PLATFORMS_BULB = [Platform.LIGHT]
|
||||
_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:
|
||||
"""Set up myStrom from a config entry."""
|
||||
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)
|
||||
raise ConfigEntryNotReady() from err
|
||||
|
||||
info.setdefault("type", 101)
|
||||
|
||||
device_type = info["type"]
|
||||
if device_type in [101, 106, 107]:
|
||||
device = MyStromSwitch(host)
|
||||
device = _get_mystrom_switch(host)
|
||||
platforms = PLATFORMS_SWITCH
|
||||
elif device_type == 102:
|
||||
await _async_get_device_state(device, info["ip"])
|
||||
elif device_type in [102, 105]:
|
||||
mac = info["mac"]
|
||||
device = MyStromBulb(host, mac)
|
||||
device = _get_mystrom_bulb(host, mac)
|
||||
platforms = PLATFORMS_BULB
|
||||
await _async_get_device_state(device, info["ip"])
|
||||
if device.bulb_type not in ["rgblamp", "strip"]:
|
||||
_LOGGER.error(
|
||||
"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)
|
||||
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(
|
||||
device=device,
|
||||
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:
|
||||
"""Unload a config entry."""
|
||||
device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
|
||||
platforms = []
|
||||
if device_type in [101, 106, 107]:
|
||||
platforms = PLATFORMS_SWITCH
|
||||
elif device_type == 102:
|
||||
platforms = PLATFORMS_BULB
|
||||
platforms.extend(PLATFORMS_SWITCH)
|
||||
elif device_type in [102, 105]:
|
||||
platforms.extend(PLATFORMS_BULB)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/python_script",
|
||||
"loggers": ["RestrictedPython"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["RestrictedPython==6.0"]
|
||||
"requirements": ["RestrictedPython==6.1"]
|
||||
}
|
||||
|
@@ -2,10 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController
|
||||
from pyrainbird.exceptions import RainbirdApiException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER
|
||||
@@ -29,11 +31,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
)
|
||||
try:
|
||||
model_info = await controller.get_model_and_version()
|
||||
except RainbirdApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
coordinator = RainbirdUpdateCoordinator(
|
||||
hass,
|
||||
name=entry.title,
|
||||
controller=controller,
|
||||
serial_number=entry.data[CONF_SERIAL_NUMBER],
|
||||
model_info=model_info,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@@ -9,6 +9,7 @@ from typing import TypeVar
|
||||
|
||||
import async_timeout
|
||||
from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException
|
||||
from pyrainbird.data import ModelAndVersion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -42,6 +43,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
name: str,
|
||||
controller: AsyncRainbirdController,
|
||||
serial_number: str,
|
||||
model_info: ModelAndVersion,
|
||||
) -> None:
|
||||
"""Initialize ZoneStateUpdateCoordinator."""
|
||||
super().__init__(
|
||||
@@ -54,6 +56,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
self._controller = controller
|
||||
self._serial_number = serial_number
|
||||
self._zones: set[int] | None = None
|
||||
self._model_info = model_info
|
||||
|
||||
@property
|
||||
def controller(self) -> AsyncRainbirdController:
|
||||
@@ -72,6 +75,8 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
default_name=f"{MANUFACTURER} Controller",
|
||||
identifiers={(DOMAIN, self._serial_number)},
|
||||
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:
|
||||
|
@@ -62,6 +62,7 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity):
|
||||
"""Define a RainMachine update entity."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
| UpdateEntityFeature.PROGRESS
|
||||
|
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.7.1"]
|
||||
"requirements": ["reolink-aio==0.7.3"]
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ SELECT_ENTITIES = (
|
||||
icon="mdi:spotlight-beam",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
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"),
|
||||
value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name,
|
||||
method=lambda api, ch, name: api.set_whiteled(ch, mode=name),
|
||||
|
@@ -62,7 +62,9 @@
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"auto": "Auto",
|
||||
"schedule": "Schedule"
|
||||
"schedule": "Schedule",
|
||||
"adaptive": "Adaptive",
|
||||
"autoadaptive": "Auto adaptive"
|
||||
}
|
||||
},
|
||||
"day_night_mode": {
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioridwell"],
|
||||
"requirements": ["aioridwell==2023.01.0"]
|
||||
"requirements": ["aioridwell==2023.07.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": ["python-roborock==0.29.2"]
|
||||
"requirements": ["python-roborock==0.30.0"]
|
||||
}
|
||||
|
@@ -90,6 +90,7 @@ class SlimProtoPlayer(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
)
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, slimserver: SlimServer, player: SlimClient) -> None:
|
||||
"""Initialize MediaPlayer entity."""
|
||||
|
@@ -33,6 +33,7 @@ class StookwijzerSensor(SensorEntity):
|
||||
_attr_attribution = "Data provided by stookwijzer.nu"
|
||||
_attr_device_class = SensorDeviceClass.ENUM
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_translation_key = "stookwijzer"
|
||||
|
||||
def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None:
|
||||
|
@@ -122,6 +122,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
)
|
||||
_attr_name = None
|
||||
_attr_translation_key = "cover"
|
||||
CLOSED_UP_THRESHOLD = 80
|
||||
CLOSED_DOWN_THRESHOLD = 20
|
||||
|
@@ -49,9 +49,10 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class UpbLight(UpbAttachedEntity, LightEntity):
|
||||
"""Representation of an UPB Light."""
|
||||
"""Representation of a UPB Light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, element, unique_id, upb):
|
||||
"""Initialize an UpbLight."""
|
||||
|
@@ -47,7 +47,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class UpbLink(UpbEntity, Scene):
|
||||
"""Representation of an UPB Link."""
|
||||
"""Representation of a UPB Link."""
|
||||
|
||||
def __init__(self, element, unique_id, upb):
|
||||
"""Initialize the base of all UPB devices."""
|
||||
|
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pywemo"],
|
||||
"requirements": ["pywemo==0.9.1"],
|
||||
"requirements": ["pywemo==1.1.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Belkin International Inc."
|
||||
|
@@ -43,6 +43,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity):
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None:
|
||||
"""Initialize the Yale Alarm Device."""
|
||||
|
@@ -40,6 +40,8 @@ async def async_setup_entry(
|
||||
class YaleDoorlock(YaleEntity, LockEntity):
|
||||
"""Representation of a Yale doorlock."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, coordinator: YaleDataUpdateCoordinator, data: dict, code_format: int
|
||||
) -> None:
|
||||
|
@@ -29,6 +29,7 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity):
|
||||
"""A yale xs ble lock."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
@callback
|
||||
def _async_update_state(
|
||||
|
@@ -25,10 +25,10 @@
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.101",
|
||||
"zigpy-deconz==0.21.0",
|
||||
"zigpy==0.56.1",
|
||||
"zigpy==0.56.2",
|
||||
"zigpy-xbee==0.18.1",
|
||||
"zigpy-zigate==0.11.0",
|
||||
"zigpy-znp==0.11.2"
|
||||
"zigpy-znp==0.11.3"
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
@@ -16,7 +16,6 @@ from collections.abc import (
|
||||
)
|
||||
import concurrent.futures
|
||||
from contextlib import suppress
|
||||
from contextvars import ContextVar
|
||||
import datetime
|
||||
import enum
|
||||
import functools
|
||||
@@ -156,8 +155,6 @@ MAX_EXPECTED_ENTITY_IDS = 16384
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_cv_hass: ContextVar[HomeAssistant] = ContextVar("hass")
|
||||
|
||||
|
||||
@functools.lru_cache(MAX_EXPECTED_ENTITY_IDS)
|
||||
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
|
||||
|
||||
|
||||
class _Hass(threading.local):
|
||||
"""Container which makes a HomeAssistant instance available to the event loop."""
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
|
||||
|
||||
_hass = _Hass()
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_hass() -> HomeAssistant:
|
||||
"""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
|
||||
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
|
||||
@@ -293,9 +301,9 @@ class HomeAssistant:
|
||||
config_entries: ConfigEntries = None # type: ignore[assignment]
|
||||
|
||||
def __new__(cls) -> HomeAssistant:
|
||||
"""Set the _cv_hass context variable."""
|
||||
"""Set the _hass thread local data."""
|
||||
hass = super().__new__(cls)
|
||||
_cv_hass.set(hass)
|
||||
_hass.hass = hass
|
||||
return hass
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -1760,7 +1768,7 @@ class ServiceRegistry:
|
||||
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.
|
||||
"""
|
||||
if not (handler := self._services[domain][service]):
|
||||
if not (handler := self._services[domain.lower()][service.lower()]):
|
||||
return SupportsResponse.NONE
|
||||
return handler.supports_response
|
||||
|
||||
|
@@ -93,7 +93,7 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||
from homeassistant.generated import currencies
|
||||
from homeassistant.generated.countries import COUNTRIES
|
||||
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")
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with contextlib.suppress(LookupError):
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
hass = async_get_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")
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with contextlib.suppress(LookupError):
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
|
||||
template_value = template_helper.Template(str(value), hass)
|
||||
@@ -1098,7 +1098,7 @@ def _no_yaml_config_schema(
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
with contextlib.suppress(LookupError):
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
async_create_issue(
|
||||
hass,
|
||||
|
@@ -670,6 +670,9 @@ def async_set_service_schema(
|
||||
hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
|
||||
) -> None:
|
||||
"""Register a description for a service."""
|
||||
domain = domain.lower()
|
||||
service = service.lower()
|
||||
|
||||
descriptions_cache: dict[
|
||||
tuple[str, str], dict[str, Any] | None
|
||||
] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.7.1"
|
||||
version = "2023.7.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@@ -124,7 +124,7 @@ PyXiaomiGateway==0.14.3
|
||||
RachioPy==1.0.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
RestrictedPython==6.0
|
||||
RestrictedPython==6.1
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@@ -252,7 +252,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.6.5
|
||||
aiohomekit==2.6.7
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -330,7 +330,7 @@ aioqsw==0.3.2
|
||||
aiorecollect==1.0.8
|
||||
|
||||
# homeassistant.components.ridwell
|
||||
aioridwell==2023.01.0
|
||||
aioridwell==2023.07.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -565,7 +565,7 @@ brunt==1.2.0
|
||||
bt-proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.12.0
|
||||
bthome-ble==2.12.1
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -864,7 +864,7 @@ gitterpy==0.1.7
|
||||
glances-api==0.4.3
|
||||
|
||||
# homeassistant.components.goalzero
|
||||
goalzero==0.2.1
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.2.31
|
||||
@@ -1615,7 +1615,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==1.9.73
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.9.0
|
||||
pydaikin==2.10.5
|
||||
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
@@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.29.2
|
||||
python-roborock==0.30.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@@ -2213,7 +2213,7 @@ pyvolumio==0.1.5
|
||||
pywebpush==1.9.2
|
||||
|
||||
# homeassistant.components.wemo
|
||||
pywemo==0.9.1
|
||||
pywemo==1.1.0
|
||||
|
||||
# homeassistant.components.wilight
|
||||
pywilight==0.0.74
|
||||
@@ -2267,7 +2267,7 @@ renault-api==0.1.13
|
||||
renson-endura-delta==1.5.0
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.7.1
|
||||
reolink-aio==0.7.3
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@@ -2759,10 +2759,10 @@ zigpy-xbee==0.18.1
|
||||
zigpy-zigate==0.11.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.11.2
|
||||
zigpy-znp==0.11.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.56.1
|
||||
zigpy==0.56.2
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
@@ -108,7 +108,7 @@ PyXiaomiGateway==0.14.3
|
||||
RachioPy==1.0.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
RestrictedPython==6.0
|
||||
RestrictedPython==6.1
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@@ -227,7 +227,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.6.5
|
||||
aiohomekit==2.6.7
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -302,7 +302,7 @@ aioqsw==0.3.2
|
||||
aiorecollect==1.0.8
|
||||
|
||||
# homeassistant.components.ridwell
|
||||
aioridwell==2023.01.0
|
||||
aioridwell==2023.07.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -469,7 +469,7 @@ brottsplatskartan==0.0.1
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.12.0
|
||||
bthome-ble==2.12.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
@@ -677,7 +677,7 @@ gios==3.1.0
|
||||
glances-api==0.4.3
|
||||
|
||||
# homeassistant.components.goalzero
|
||||
goalzero==0.2.1
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.2.31
|
||||
@@ -1197,7 +1197,7 @@ pycoolmasternet-async==0.1.5
|
||||
pycsspeechtts==1.0.8
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.9.0
|
||||
pydaikin==2.10.5
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==113
|
||||
@@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0
|
||||
python-qbittorrent==0.4.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.29.2
|
||||
python-roborock==0.30.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@@ -1621,7 +1621,7 @@ pyvolumio==0.1.5
|
||||
pywebpush==1.9.2
|
||||
|
||||
# homeassistant.components.wemo
|
||||
pywemo==0.9.1
|
||||
pywemo==1.1.0
|
||||
|
||||
# homeassistant.components.wilight
|
||||
pywilight==0.0.74
|
||||
@@ -1660,7 +1660,7 @@ renault-api==0.1.13
|
||||
renson-endura-delta==1.5.0
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.7.1
|
||||
reolink-aio==0.7.3
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.65
|
||||
@@ -2023,10 +2023,10 @@ zigpy-xbee==0.18.1
|
||||
zigpy-zigate==0.11.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.11.2
|
||||
zigpy-znp==0.11.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.56.1
|
||||
zigpy==0.56.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.49.0
|
||||
|
128
tests/components/daikin/test_init.py
Normal file
128
tests/components/daikin/test_init.py
Normal 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
|
@@ -1308,3 +1308,45 @@ async def test_option_flow(
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: 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
|
||||
|
@@ -58,7 +58,9 @@ async def test_setup_dashboard_fails(
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
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(
|
||||
|
@@ -8,6 +8,9 @@
|
||||
# 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'
|
||||
# ---
|
||||
# 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]
|
||||
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'
|
||||
# ---
|
||||
|
@@ -60,11 +60,31 @@ GUEST_WIFI_CHANGED: dict[str, dict] = {
|
||||
|
||||
GUEST_WIFI_DISABLED: dict[str, dict] = {
|
||||
"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(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
@@ -150,23 +170,3 @@ async def test_image_update(
|
||||
|
||||
assert resp_body != resp_body_new
|
||||
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
|
||||
|
@@ -1 +1,174 @@
|
||||
"""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"]
|
||||
|
@@ -2,11 +2,19 @@
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from pymystrom.exceptions import MyStromConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.mystrom.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
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 tests.common import MockConfigEntry
|
||||
@@ -16,36 +24,27 @@ async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
device_type: int,
|
||||
bulb_type: str = "strip",
|
||||
) -> None:
|
||||
"""Inititialize integration for testing."""
|
||||
with patch(
|
||||
"pymystrom.get_device_info",
|
||||
side_effect=AsyncMock(return_value={"type": device_type, "mac": DEVICE_MAC}),
|
||||
), patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), patch(
|
||||
"pymystrom.bulb.MyStromBulb.get_state", return_value={}
|
||||
side_effect=AsyncMock(return_value=get_default_device_response(device_type)),
|
||||
), patch(
|
||||
"pymystrom.bulb.MyStromBulb.bulb_type", bulb_type
|
||||
"homeassistant.components.mystrom._get_mystrom_bulb",
|
||||
return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()),
|
||||
), patch(
|
||||
"pymystrom.switch.MyStromSwitch.mac",
|
||||
new_callable=PropertyMock,
|
||||
return_value=DEVICE_MAC,
|
||||
), patch(
|
||||
"pymystrom.bulb.MyStromBulb.mac",
|
||||
new_callable=PropertyMock,
|
||||
return_value=DEVICE_MAC,
|
||||
"homeassistant.components.mystrom._get_mystrom_switch",
|
||||
return_value=MyStromSwitchMock(get_default_switch_state()),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_init_switch_and_unload(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""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")
|
||||
assert state is not None
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
@@ -56,12 +55,35 @@ async def test_init_switch_and_unload(
|
||||
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."""
|
||||
await init_integration(hass, config_entry, 102)
|
||||
state = hass.states.get("light.mystrom_device")
|
||||
assert state is not None
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
await init_integration(hass, config_entry, device_type)
|
||||
state = hass.states.get(f"{platform}.mystrom_device")
|
||||
assert (state is None) == entity_state_none
|
||||
assert config_entry.state is entry_state
|
||||
|
||||
|
||||
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."""
|
||||
with patch(
|
||||
"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(
|
||||
"pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError()
|
||||
), 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.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
@@ -35,6 +35,8 @@ SERIAL_NUMBER = 0x12635436566
|
||||
|
||||
# Get serial number Command 0x85. Serial is 0x12635436566
|
||||
SERIAL_RESPONSE = "850000012635436566"
|
||||
# Model and version command 0x82
|
||||
MODEL_AND_VERSION_RESPONSE = "820006090C"
|
||||
# Get available stations command 0x83
|
||||
AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones
|
||||
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.
|
||||
"""
|
||||
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")
|
||||
|
@@ -70,6 +70,8 @@ async def test_set_value(
|
||||
device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)})
|
||||
assert device
|
||||
assert device.name == "Rain Bird Controller"
|
||||
assert device.model == "ST8x-WiFi"
|
||||
assert device.sw_version == "9.12"
|
||||
|
||||
aioclient_mock.mock_calls.clear()
|
||||
responses.append(mock_response(ACK_ECHO))
|
||||
|
@@ -1,10 +1,11 @@
|
||||
"""Test zha siren."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, call, patch
|
||||
|
||||
import pytest
|
||||
from zigpy.const import SIG_EP_PROFILE
|
||||
import zigpy.profiles.zha as zha
|
||||
import zigpy.zcl
|
||||
import zigpy.zcl.clusters.general as general
|
||||
import zigpy.zcl.clusters.security as security
|
||||
import zigpy.zcl.foundation as zcl_f
|
||||
@@ -85,48 +86,76 @@ async def test_siren(hass: HomeAssistant, siren) -> None:
|
||||
|
||||
# turn on from HA
|
||||
with patch(
|
||||
"zigpy.zcl.Cluster.request",
|
||||
"zigpy.device.Device.request",
|
||||
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
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
|
||||
)
|
||||
assert len(cluster.request.mock_calls) == 1
|
||||
assert cluster.request.call_args[0][0] is False
|
||||
assert cluster.request.call_args[0][1] == 0
|
||||
assert cluster.request.call_args[0][3] == 50 # bitmask for default args
|
||||
assert cluster.request.call_args[0][4] == 5 # duration in seconds
|
||||
assert cluster.request.call_args[0][5] == 0
|
||||
assert cluster.request.call_args[0][6] == 2
|
||||
assert cluster.request.mock_calls == [
|
||||
call(
|
||||
cluster,
|
||||
False,
|
||||
0,
|
||||
ANY,
|
||||
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
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
# turn off from HA
|
||||
with patch(
|
||||
"zigpy.zcl.Cluster.request",
|
||||
"zigpy.device.Device.request",
|
||||
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
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True
|
||||
)
|
||||
assert len(cluster.request.mock_calls) == 1
|
||||
assert cluster.request.call_args[0][0] is False
|
||||
assert cluster.request.call_args[0][1] == 0
|
||||
assert cluster.request.call_args[0][3] == 2 # bitmask for default args
|
||||
assert cluster.request.call_args[0][4] == 5 # duration in seconds
|
||||
assert cluster.request.call_args[0][5] == 0
|
||||
assert cluster.request.call_args[0][6] == 2
|
||||
assert cluster.request.mock_calls == [
|
||||
call(
|
||||
cluster,
|
||||
False,
|
||||
0,
|
||||
ANY,
|
||||
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
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
# turn on from HA
|
||||
with patch(
|
||||
"zigpy.zcl.Cluster.request",
|
||||
"zigpy.device.Device.request",
|
||||
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
|
||||
await hass.services.async_call(
|
||||
@@ -140,14 +169,21 @@ async def test_siren(hass: HomeAssistant, siren) -> None:
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(cluster.request.mock_calls) == 1
|
||||
assert cluster.request.call_args[0][0] is False
|
||||
assert cluster.request.call_args[0][1] == 0
|
||||
assert cluster.request.call_args[0][3] == 97 # bitmask for passed args
|
||||
assert cluster.request.call_args[0][4] == 10 # duration in seconds
|
||||
assert cluster.request.call_args[0][5] == 0
|
||||
assert cluster.request.call_args[0][6] == 2
|
||||
|
||||
assert cluster.request.mock_calls == [
|
||||
call(
|
||||
cluster,
|
||||
False,
|
||||
0,
|
||||
ANY,
|
||||
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
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
|
@@ -490,17 +490,7 @@ def hass_fixture_setup() -> list[bool]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hass(_hass: HomeAssistant) -> HomeAssistant:
|
||||
"""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(
|
||||
async def hass(
|
||||
hass_fixture_setup: list[bool],
|
||||
event_loop: asyncio.AbstractEventLoop,
|
||||
load_registries: bool,
|
||||
|
@@ -12,6 +12,7 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
issue_registry as ir,
|
||||
@@ -383,7 +384,7 @@ def test_service() -> None:
|
||||
schema("homeassistant.turn_on")
|
||||
|
||||
|
||||
def test_service_schema() -> None:
|
||||
def test_service_schema(hass: HomeAssistant) -> None:
|
||||
"""Test service_schema validation."""
|
||||
options = (
|
||||
{},
|
||||
@@ -1550,10 +1551,10 @@ def test_config_entry_only_schema_cant_find_module() -> None:
|
||||
def test_config_entry_only_schema_no_hass(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> 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(
|
||||
"homeassistant.helpers.config_validation.async_get_hass",
|
||||
side_effect=LookupError,
|
||||
side_effect=HomeAssistantError,
|
||||
):
|
||||
cv.config_entry_only_config_schema("test_domain")(
|
||||
{"test_domain": {"foo": "bar"}}
|
||||
|
@@ -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:
|
||||
"""Test service calls invoked only if entity has required features."""
|
||||
test_service_mock = AsyncMock(return_value=None)
|
||||
|
@@ -9,10 +9,12 @@ import gc
|
||||
import logging
|
||||
import os
|
||||
from tempfile import TemporaryDirectory
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
import async_timeout
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -40,6 +42,7 @@ from homeassistant.core import (
|
||||
ServiceResponse,
|
||||
State,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""Simulate a shutdown, test calling stuff."""
|
||||
test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP)
|
||||
|
Reference in New Issue
Block a user