mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
2024.2.2 (#110720)
This commit is contained in:
commit
7aa14e20d1
@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin):
|
||||
device = self.get_device_detail(device_id)
|
||||
activities = activities_from_pubnub_message(device, date_time, message)
|
||||
activity_stream = self.activity_stream
|
||||
if activities:
|
||||
activity_stream.async_process_newer_device_activities(activities)
|
||||
if activities and activity_stream.async_process_newer_device_activities(
|
||||
activities
|
||||
):
|
||||
self.async_signal_device_id_update(device.device_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
|
||||
@callback
|
||||
def async_stop(self) -> None:
|
||||
|
@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.1"]
|
||||
"requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"]
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from aioelectricitymaps import (
|
||||
ElectricityMaps,
|
||||
ElectricityMapsError,
|
||||
ElectricityMapsInvalidTokenError,
|
||||
ElectricityMapsNoDataError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@ -151,6 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await fetch_latest_carbon_intensity(self.hass, em, data)
|
||||
except ElectricityMapsInvalidTokenError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ElectricityMapsNoDataError:
|
||||
errors["base"] = "no_data"
|
||||
except ElectricityMapsError:
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
@ -28,12 +28,9 @@
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "API Ratelimit exceeded"
|
||||
"no_data": "No data is available for the location you have selected."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["async-upnp-client==0.38.1"],
|
||||
"requirements": ["async-upnp-client==0.38.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.1"]
|
||||
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"]
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@ -398,22 +398,30 @@ async def async_wait_for_elk_to_sync(
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
def _getelk(service: ServiceCall) -> Elk:
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_word(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_phrase(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.set_time(dt_util.now())
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"requirements": ["evohome-async==0.4.18"]
|
||||
"requirements": ["evohome-async==0.4.19"]
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
from .router import get_api
|
||||
from .router import get_api, get_hosts_list_if_supported
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -69,7 +69,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check permissions
|
||||
await fbx.system.get_config()
|
||||
await fbx.lan.get_hosts_list()
|
||||
await get_hosts_list_if_supported(fbx)
|
||||
|
||||
# Close connection
|
||||
await fbx.close()
|
||||
|
@ -64,6 +64,33 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
|
||||
return Freepybox(APP_DESC, token_file, API_VERSION)
|
||||
|
||||
|
||||
async def get_hosts_list_if_supported(
|
||||
fbx_api: Freepybox,
|
||||
) -> tuple[bool, list[dict[str, Any]]]:
|
||||
"""Hosts list is not supported when freebox is configured in bridge mode."""
|
||||
supports_hosts: bool = True
|
||||
fbx_devices: list[dict[str, Any]] = []
|
||||
try:
|
||||
fbx_devices = await fbx_api.lan.get_hosts_list() or []
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
|
||||
return supports_hosts, fbx_devices
|
||||
|
||||
|
||||
class FreeboxRouter:
|
||||
"""Representation of a Freebox router."""
|
||||
|
||||
@ -111,27 +138,9 @@ class FreeboxRouter:
|
||||
|
||||
# Access to Host list not available in bridge mode, API return error_code 'nodev'
|
||||
if self.supports_hosts:
|
||||
try:
|
||||
fbx_devices = await self._api.lan.get_hosts_list()
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(
|
||||
matcher := re.search(
|
||||
r"Request failed \(APIResponse: (.+)\)", str(err)
|
||||
)
|
||||
)
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
self.supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
self.supports_hosts, fbx_devices = await get_hosts_list_if_supported(
|
||||
self._api
|
||||
)
|
||||
|
||||
# Adds the Freebox itself
|
||||
fbx_devices.append(
|
||||
|
@ -476,7 +476,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source_entities": ", ".join(self._entity_ids),
|
||||
"state_classes:": ", ".join(state_classes),
|
||||
"state_classes": ", ".join(state_classes),
|
||||
},
|
||||
)
|
||||
return None
|
||||
@ -519,7 +519,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source_entities": ", ".join(self._entity_ids),
|
||||
"device_classes:": ", ".join(device_classes),
|
||||
"device_classes": ", ".join(device_classes),
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
@ -265,7 +265,7 @@
|
||||
},
|
||||
"state_classes_not_matching": {
|
||||
"title": "State classes is not correct",
|
||||
"description": "Device classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue."
|
||||
"description": "State classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.12.0",
|
||||
"xknxproject==3.5.0",
|
||||
"xknxproject==3.6.0",
|
||||
"knx-frontend==2024.1.20.105944"
|
||||
]
|
||||
}
|
||||
|
@ -6,11 +6,12 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from linear_garage_door import Linear
|
||||
from linear_garage_door.errors import InvalidLoginError, ResponseError
|
||||
from linear_garage_door.errors import InvalidLoginError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -55,6 +56,7 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
email=self._email,
|
||||
password=self._password,
|
||||
device_id=self._device_id,
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
except InvalidLoginError as err:
|
||||
if (
|
||||
@ -63,8 +65,6 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except ResponseError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if not self._devices:
|
||||
self._devices = await linear.get_devices(self._site_id)
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/linear_garage_door",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["linear-garage-door==0.2.7"]
|
||||
"requirements": ["linear-garage-door==0.2.9"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lutron",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pylutron"],
|
||||
"requirements": ["pylutron==0.2.8"]
|
||||
"requirements": ["pylutron==0.2.12"]
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import datapoint
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
@ -16,7 +17,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
@ -34,9 +35,6 @@ from .const import (
|
||||
from .data import MetOfficeData
|
||||
from .helpers import fetch_data, fetch_site
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
import datapoint
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
@ -44,10 +42,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Met Office entry."""
|
||||
if sys.version_info >= (3, 12):
|
||||
raise HomeAssistantError(
|
||||
"Met Office is not supported on Python 3.12. Please use Python 3.11."
|
||||
)
|
||||
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
|
@ -2,12 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Site import Site
|
||||
from datapoint.Timestep import Timestep
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Site import Site
|
||||
from datapoint.Timestep import Timestep
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -2,7 +2,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import datapoint
|
||||
from datapoint.Site import Site
|
||||
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
from homeassistant.util.dt import utcnow
|
||||
@ -10,11 +12,6 @@ from homeassistant.util.dt import utcnow
|
||||
from .const import MODE_3HOURLY
|
||||
from .data import MetOfficeData
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
import datapoint
|
||||
from datapoint.Site import Site
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -34,7 +31,7 @@ def fetch_site(
|
||||
def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData:
|
||||
"""Fetch weather and forecast from Datapoint API."""
|
||||
try:
|
||||
forecast = connection.get_forecast_for_site(site.id, mode)
|
||||
forecast = connection.get_forecast_for_site(site.location_id, mode)
|
||||
except (ValueError, datapoint.exceptions.APIException) as err:
|
||||
_LOGGER.error("Check Met Office connection: %s", err.args)
|
||||
raise UpdateFailed from err
|
||||
|
@ -3,9 +3,8 @@
|
||||
"name": "Met Office",
|
||||
"codeowners": ["@MrHarcombe", "@avee87"],
|
||||
"config_flow": true,
|
||||
"disabled": "Integration library not compatible with Python 3.12",
|
||||
"documentation": "https://www.home-assistant.io/integrations/metoffice",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["datapoint"],
|
||||
"requirements": ["datapoint==0.9.8;python_version<'3.12'"]
|
||||
"requirements": ["datapoint==0.9.9"]
|
||||
}
|
||||
|
@ -251,6 +251,6 @@ class MetOfficeCurrentSensor(
|
||||
return {
|
||||
ATTR_LAST_UPDATE: self.coordinator.data.now.date,
|
||||
ATTR_SENSOR_ID: self.entity_description.key,
|
||||
ATTR_SITE_ID: self.coordinator.data.site.id,
|
||||
ATTR_SITE_ID: self.coordinator.data.site.location_id,
|
||||
ATTR_SITE_NAME: self.coordinator.data.site.name,
|
||||
}
|
||||
|
@ -199,6 +199,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
|
||||
self._precision = config.get(CONF_PRECISION, 2)
|
||||
else:
|
||||
self._precision = config.get(CONF_PRECISION, 0)
|
||||
if self._precision > 0 or self._scale != int(self._scale):
|
||||
self._value_is_int = False
|
||||
|
||||
def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
|
||||
"""Do swap as needed."""
|
||||
|
@ -400,6 +400,7 @@ class MotionTDBUDevice(MotionPositionDevice):
|
||||
def __init__(self, coordinator, blind, device_class, motor):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator, blind, device_class)
|
||||
delattr(self, "_attr_name")
|
||||
self._motor = motor
|
||||
self._motor_key = motor[0]
|
||||
self._attr_translation_key = motor.lower()
|
||||
|
@ -212,7 +212,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
host = entry.data[CONF_HOST]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
if not (data := hass.data.get(DOMAIN)) or data.websession.closed:
|
||||
websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
|
||||
|
||||
hass.data[DOMAIN] = LTEData(websession)
|
||||
@ -258,7 +258,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
hass.data.pop(DOMAIN)
|
||||
hass.data.pop(DOMAIN, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiopegelonline"],
|
||||
"requirements": ["aiopegelonline==0.0.6"]
|
||||
"requirements": ["aiopegelonline==0.0.8"]
|
||||
}
|
||||
|
@ -21,7 +21,10 @@ from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
HVACAction,
|
||||
)
|
||||
from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
@ -437,7 +440,7 @@ class PrometheusMetrics:
|
||||
float(cover_state == state.state)
|
||||
)
|
||||
|
||||
position = state.attributes.get(ATTR_POSITION)
|
||||
position = state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if position is not None:
|
||||
position_metric = self._metric(
|
||||
"cover_position",
|
||||
@ -446,7 +449,7 @@ class PrometheusMetrics:
|
||||
)
|
||||
position_metric.labels(**self._labels(state)).set(float(position))
|
||||
|
||||
tilt_position = state.attributes.get(ATTR_TILT_POSITION)
|
||||
tilt_position = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
|
||||
if tilt_position is not None:
|
||||
tilt_position_metric = self._metric(
|
||||
"cover_tilt_position",
|
||||
|
@ -115,6 +115,7 @@ async def setup_device(
|
||||
device.name,
|
||||
)
|
||||
_LOGGER.debug(err)
|
||||
await mqtt_client.async_release()
|
||||
raise err
|
||||
coordinator = RoborockDataUpdateCoordinator(
|
||||
hass, device, networking, product_info, mqtt_client
|
||||
@ -125,6 +126,7 @@ async def setup_device(
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady:
|
||||
await coordinator.release()
|
||||
if isinstance(coordinator.api, RoborockMqttClient):
|
||||
_LOGGER.warning(
|
||||
"Not setting up %s because the we failed to get data for the first time using the online client. "
|
||||
@ -153,14 +155,10 @@ async def setup_device(
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.release()
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id].values()
|
||||
)
|
||||
)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
release_tasks = set()
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id].values():
|
||||
release_tasks.add(coordinator.release())
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
await asyncio.gather(*release_tasks)
|
||||
return unload_ok
|
||||
|
@ -77,7 +77,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
|
||||
async def release(self) -> None:
|
||||
"""Disconnect from API."""
|
||||
await self.api.async_disconnect()
|
||||
await self.api.async_release()
|
||||
await self.cloud_api.async_release()
|
||||
|
||||
async def _update_device_prop(self) -> None:
|
||||
"""Update device properties."""
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""Support for Roborock device base class."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from roborock.api import AttributeCache, RoborockClient
|
||||
@ -7,6 +6,7 @@ from roborock.cloud_api import RoborockMqttClient
|
||||
from roborock.command_cache import CacheableAttribute
|
||||
from roborock.containers import Consumable, Status
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_message import RoborockDataProtocol
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -24,7 +24,10 @@ class RoborockEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, unique_id: str, device_info: DeviceInfo, api: RoborockClient
|
||||
self,
|
||||
unique_id: str,
|
||||
device_info: DeviceInfo,
|
||||
api: RoborockClient,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
self._attr_unique_id = unique_id
|
||||
@ -75,6 +78,9 @@ class RoborockCoordinatedEntity(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
listener_request: list[RoborockDataProtocol]
|
||||
| RoborockDataProtocol
|
||||
| None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
RoborockEntity.__init__(
|
||||
@ -85,6 +91,23 @@ class RoborockCoordinatedEntity(
|
||||
)
|
||||
CoordinatorEntity.__init__(self, coordinator=coordinator)
|
||||
self._attr_unique_id = unique_id
|
||||
if isinstance(listener_request, RoborockDataProtocol):
|
||||
listener_request = [listener_request]
|
||||
self.listener_requests = listener_request or []
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add listeners when the device is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
for listener_request in self.listener_requests:
|
||||
self.api.add_listener(
|
||||
listener_request, self._update_from_listener, cache=self.api.cache
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove listeners when the device is removed from hass."""
|
||||
for listener_request in self.listener_requests:
|
||||
self.api.remove_listener(listener_request, self._update_from_listener)
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
@property
|
||||
def _device_status(self) -> Status:
|
||||
@ -107,7 +130,7 @@ class RoborockCoordinatedEntity(
|
||||
await self.coordinator.async_refresh()
|
||||
return res
|
||||
|
||||
def _update_from_listener(self, value: Status | Consumable):
|
||||
def _update_from_listener(self, value: Status | Consumable) -> None:
|
||||
"""Update the status or consumable data from a listener and then write the new entity state."""
|
||||
if isinstance(value, Status):
|
||||
self.coordinator.roborock_device_info.props.status = value
|
||||
|
@ -107,10 +107,8 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity):
|
||||
) -> None:
|
||||
"""Create a select entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(unique_id, coordinator)
|
||||
super().__init__(unique_id, coordinator, entity_description.protocol_listener)
|
||||
self._attr_options = options
|
||||
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the option."""
|
||||
|
@ -232,10 +232,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity):
|
||||
description: RoborockSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(unique_id, coordinator)
|
||||
self.entity_description = description
|
||||
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||
super().__init__(unique_id, coordinator, description.protocol_listener)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime.datetime:
|
||||
|
@ -92,14 +92,16 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
|
||||
) -> None:
|
||||
"""Initialize a vacuum."""
|
||||
StateVacuumEntity.__init__(self)
|
||||
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
|
||||
RoborockCoordinatedEntity.__init__(
|
||||
self,
|
||||
unique_id,
|
||||
coordinator,
|
||||
listener_request=[
|
||||
RoborockDataProtocol.FAN_POWER,
|
||||
RoborockDataProtocol.STATE,
|
||||
],
|
||||
)
|
||||
self._attr_fan_speed_list = self._device_status.fan_power_options
|
||||
self.api.add_listener(
|
||||
RoborockDataProtocol.FAN_POWER, self._update_from_listener, self.api.cache
|
||||
)
|
||||
self.api.add_listener(
|
||||
RoborockDataProtocol.STATE, self._update_from_listener, self.api.cache
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
|
@ -39,7 +39,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.6.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.38.1"
|
||||
"async-upnp-client==0.38.2"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
@ -173,9 +173,9 @@ async def async_setup_entry(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ENABLE_CLIMATE_REACT,
|
||||
{
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float,
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict,
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float,
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict,
|
||||
vol.Required(ATTR_SMART_TYPE): vol.In(
|
||||
["temperature", "feelsLike", "humidity"]
|
||||
|
@ -117,7 +117,7 @@
|
||||
"speed": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"sulfur_dioxide": {
|
||||
"sulphur_dioxide": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"temperature": {
|
||||
|
@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["async-upnp-client==0.38.1"]
|
||||
"requirements": ["async-upnp-client==0.38.2"]
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ class StarlineSensor(StarlineEntity, SensorEntity):
|
||||
if self._key == "mileage" and self._device.mileage:
|
||||
return self._device.mileage.get("val")
|
||||
if self._key == "gps_count" and self._device.position:
|
||||
return self._device.position["sat_qty"]
|
||||
return self._device.position.get("sat_qty")
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -39,5 +39,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"requirements": ["PySwitchbot==0.44.0"]
|
||||
"requirements": ["PySwitchbot==0.45.0"]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""DataUpdateCoordinators for the System monitor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
@ -43,7 +44,8 @@ dataT = TypeVar(
|
||||
| sswap
|
||||
| VirtualMemory
|
||||
| tuple[float, float, float]
|
||||
| sdiskusage,
|
||||
| sdiskusage
|
||||
| None,
|
||||
)
|
||||
|
||||
|
||||
@ -130,12 +132,15 @@ class SystemMonitorLoadCoordinator(MonitorCoordinator[tuple[float, float, float]
|
||||
return os.getloadavg()
|
||||
|
||||
|
||||
class SystemMonitorProcessorCoordinator(MonitorCoordinator[float]):
|
||||
class SystemMonitorProcessorCoordinator(MonitorCoordinator[float | None]):
|
||||
"""A System monitor Processor Data Update Coordinator."""
|
||||
|
||||
def update_data(self) -> float:
|
||||
def update_data(self) -> float | None:
|
||||
"""Fetch data."""
|
||||
return psutil.cpu_percent(interval=None)
|
||||
cpu_percent = psutil.cpu_percent(interval=None)
|
||||
if cpu_percent > 0.0:
|
||||
return cpu_percent
|
||||
return None
|
||||
|
||||
|
||||
class SystemMonitorBootTimeCoordinator(MonitorCoordinator[datetime]):
|
||||
|
@ -344,7 +344,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = {
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon=get_cpu_icon(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda entity: round(entity.coordinator.data),
|
||||
value_fn=lambda entity: (
|
||||
round(entity.coordinator.data) if entity.coordinator.data else None
|
||||
),
|
||||
),
|
||||
"processor_temperature": SysMonitorSensorEntityDescription[
|
||||
dict[str, list[shwtemp]]
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/technove",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-technove==1.2.1"],
|
||||
"requirements": ["python-technove==1.2.2"],
|
||||
"zeroconf": ["_technove-stations._tcp.local."]
|
||||
}
|
||||
|
@ -63,7 +63,9 @@
|
||||
"state": {
|
||||
"unplugged": "Unplugged",
|
||||
"plugged_waiting": "Plugged, waiting",
|
||||
"plugged_charging": "Plugged, charging"
|
||||
"plugged_charging": "Plugged, charging",
|
||||
"out_of_activation_period": "Out of activation period",
|
||||
"high_charge_period": "High charge period"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle discovery via dhcp."""
|
||||
return await self._async_handle_discovery(
|
||||
discovery_info.ip, discovery_info.macaddress
|
||||
discovery_info.ip, dr.format_mac(discovery_info.macaddress)
|
||||
)
|
||||
|
||||
async def async_step_integration_discovery(
|
||||
|
@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==70"],
|
||||
"requirements": ["aiounifi==71"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
||||
|
@ -6,5 +6,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aio_geojson_usgs_earthquakes"],
|
||||
"requirements": ["aio-geojson-usgs-earthquakes==0.2"]
|
||||
"requirements": ["aio-geojson-usgs-earthquakes==0.3"]
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client", "yeelight"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.1"],
|
||||
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_miio._udp.local.",
|
||||
|
@ -135,7 +135,7 @@ def async_active_zone(
|
||||
is None
|
||||
# Skip zone that are outside the radius aka the
|
||||
# lat/long is outside the zone
|
||||
or not (zone_dist - (radius := zone_attrs[ATTR_RADIUS]) < radius)
|
||||
or not (zone_dist - (zone_radius := zone_attrs[ATTR_RADIUS]) < radius)
|
||||
):
|
||||
continue
|
||||
|
||||
@ -144,7 +144,7 @@ def async_active_zone(
|
||||
zone_dist < min_dist
|
||||
or (
|
||||
# If same distance, prefer smaller zone
|
||||
zone_dist == min_dist and radius < closest.attributes[ATTR_RADIUS]
|
||||
zone_dist == min_dist and zone_radius < closest.attributes[ATTR_RADIUS]
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 2
|
||||
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, 11, 0)
|
||||
|
@ -6,7 +6,7 @@ aiohttp-zlib-ng==0.3.1
|
||||
aiohttp==3.9.3
|
||||
aiohttp_cors==0.7.0
|
||||
astral==2.2
|
||||
async-upnp-client==0.38.1
|
||||
async-upnp-client==0.38.2
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==23.2.0
|
||||
awesomeversion==24.2.0
|
||||
@ -36,7 +36,7 @@ janus==1.0.0
|
||||
Jinja2==3.1.3
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
orjson==3.9.13
|
||||
orjson==3.9.14
|
||||
packaging>=23.1
|
||||
paho-mqtt==1.6.1
|
||||
Pillow==10.2.0
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,5 +4,4 @@ ARG \
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
||||
raspberrypi-utils
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.2.1"
|
||||
version = "2024.2.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -46,7 +46,7 @@ dependencies = [
|
||||
"cryptography==42.0.2",
|
||||
# pyOpenSSL 23.2.0 is required to work with cryptography 41+
|
||||
"pyOpenSSL==24.0.0",
|
||||
"orjson==3.9.13",
|
||||
"orjson==3.9.14",
|
||||
"packaging>=23.1",
|
||||
"pip>=21.3.1",
|
||||
"python-slugify==8.0.1",
|
||||
|
@ -22,7 +22,7 @@ lru-dict==1.3.0
|
||||
PyJWT==2.8.0
|
||||
cryptography==42.0.2
|
||||
pyOpenSSL==24.0.0
|
||||
orjson==3.9.13
|
||||
orjson==3.9.14
|
||||
packaging>=23.1
|
||||
pip>=21.3.1
|
||||
python-slugify==8.0.1
|
||||
|
@ -96,7 +96,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.44.0
|
||||
PySwitchbot==0.45.0
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@ -179,7 +179,7 @@ aio-geojson-geonetnz-volcano==0.9
|
||||
aio-geojson-nsw-rfs-incidents==0.7
|
||||
|
||||
# homeassistant.components.usgs_earthquakes_feed
|
||||
aio-geojson-usgs-earthquakes==0.2
|
||||
aio-geojson-usgs-earthquakes==0.3
|
||||
|
||||
# homeassistant.components.gdacs
|
||||
aio-georss-gdacs==0.9
|
||||
@ -318,7 +318,7 @@ aiooncue==0.3.5
|
||||
aioopenexchangerates==0.4.0
|
||||
|
||||
# homeassistant.components.pegel_online
|
||||
aiopegelonline==0.0.6
|
||||
aiopegelonline==0.0.8
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.4
|
||||
@ -383,7 +383,7 @@ aiotankerkoenig==0.3.0
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==70
|
||||
aiounifi==71
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@ -478,7 +478,7 @@ asterisk_mbox==0.5.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.38.1
|
||||
async-upnp-client==0.38.2
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
asyncinotify==4.0.2
|
||||
@ -671,6 +671,9 @@ crownstone-uart==2.1.0
|
||||
# homeassistant.components.datadog
|
||||
datadog==0.15.0
|
||||
|
||||
# homeassistant.components.metoffice
|
||||
datapoint==0.9.9
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==2.21.1
|
||||
|
||||
@ -684,7 +687,7 @@ debugpy==1.8.0
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==5.1.1
|
||||
deebot-client==5.2.1
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -818,7 +821,7 @@ eufylife-ble-client==0.1.8
|
||||
# evdev==1.6.1
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==0.4.18
|
||||
evohome-async==0.4.19
|
||||
|
||||
# homeassistant.components.faa_delays
|
||||
faadelays==2023.9.1
|
||||
@ -1220,7 +1223,7 @@ lightwave==0.24
|
||||
limitlessled==1.1.3
|
||||
|
||||
# homeassistant.components.linear_garage_door
|
||||
linear-garage-door==0.2.7
|
||||
linear-garage-door==0.2.9
|
||||
|
||||
# homeassistant.components.linode
|
||||
linode-api==4.1.9b1
|
||||
@ -1609,7 +1612,7 @@ py-nightscout==1.2.2
|
||||
py-schluter==0.1.7
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
py-sucks==0.9.8
|
||||
py-sucks==0.9.9
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.1.4
|
||||
@ -1928,7 +1931,7 @@ pylitterbot==2023.4.9
|
||||
pylutron-caseta==0.19.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.8
|
||||
pylutron==0.2.12
|
||||
|
||||
# homeassistant.components.mailgun
|
||||
pymailgunner==1.4
|
||||
@ -2284,7 +2287,7 @@ python-songpal==0.16.1
|
||||
python-tado==0.17.4
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==1.2.1
|
||||
python-technove==1.2.2
|
||||
|
||||
# homeassistant.components.telegram_bot
|
||||
python-telegram-bot==13.1
|
||||
@ -2859,7 +2862,7 @@ xiaomi-ble==0.23.1
|
||||
xknx==2.12.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.5.0
|
||||
xknxproject==3.6.0
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@ -2880,7 +2883,7 @@ yalesmartalarmclient==0.3.9
|
||||
yalexs-ble==2.4.1
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.10.0
|
||||
yalexs==1.11.2
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.14
|
||||
|
@ -84,7 +84,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.44.0
|
||||
PySwitchbot==0.45.0
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
PySyncThru==0.7.10
|
||||
@ -158,7 +158,7 @@ aio-geojson-geonetnz-volcano==0.9
|
||||
aio-geojson-nsw-rfs-incidents==0.7
|
||||
|
||||
# homeassistant.components.usgs_earthquakes_feed
|
||||
aio-geojson-usgs-earthquakes==0.2
|
||||
aio-geojson-usgs-earthquakes==0.3
|
||||
|
||||
# homeassistant.components.gdacs
|
||||
aio-georss-gdacs==0.9
|
||||
@ -291,7 +291,7 @@ aiooncue==0.3.5
|
||||
aioopenexchangerates==0.4.0
|
||||
|
||||
# homeassistant.components.pegel_online
|
||||
aiopegelonline==0.0.6
|
||||
aiopegelonline==0.0.8
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.4
|
||||
@ -356,7 +356,7 @@ aiotankerkoenig==0.3.0
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==70
|
||||
aiounifi==71
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@ -430,7 +430,7 @@ arcam-fmj==1.4.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.38.1
|
||||
async-upnp-client==0.38.2
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
asyncsleepiq==1.5.2
|
||||
@ -552,6 +552,9 @@ crownstone-uart==2.1.0
|
||||
# homeassistant.components.datadog
|
||||
datadog==0.15.0
|
||||
|
||||
# homeassistant.components.metoffice
|
||||
datapoint==0.9.9
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==2.21.1
|
||||
|
||||
@ -559,7 +562,7 @@ dbus-fast==2.21.1
|
||||
debugpy==1.8.0
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==5.1.1
|
||||
deebot-client==5.2.1
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -971,7 +974,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.linear_garage_door
|
||||
linear-garage-door==0.2.7
|
||||
linear-garage-door==0.2.9
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
lmcloud==0.4.35
|
||||
@ -1259,7 +1262,7 @@ py-nextbusnext==1.0.2
|
||||
py-nightscout==1.2.2
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
py-sucks==0.9.8
|
||||
py-sucks==0.9.9
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.1.4
|
||||
@ -1485,7 +1488,7 @@ pylitterbot==2023.4.9
|
||||
pylutron-caseta==0.19.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.8
|
||||
pylutron==0.2.12
|
||||
|
||||
# homeassistant.components.mailgun
|
||||
pymailgunner==1.4
|
||||
@ -1751,7 +1754,7 @@ python-songpal==0.16.1
|
||||
python-tado==0.17.4
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==1.2.1
|
||||
python-technove==1.2.2
|
||||
|
||||
# homeassistant.components.telegram_bot
|
||||
python-telegram-bot==13.1
|
||||
@ -2188,7 +2191,7 @@ xiaomi-ble==0.23.1
|
||||
xknx==2.12.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.5.0
|
||||
xknxproject==3.6.0
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@ -2206,7 +2209,7 @@ yalesmartalarmclient==0.3.9
|
||||
yalexs-ble==2.4.1
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.10.0
|
||||
yalexs==1.11.2
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.14
|
||||
|
@ -5,6 +5,7 @@ from aioelectricitymaps import (
|
||||
ElectricityMapsConnectionError,
|
||||
ElectricityMapsError,
|
||||
ElectricityMapsInvalidTokenError,
|
||||
ElectricityMapsNoDataError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
@ -139,12 +140,9 @@ async def test_form_country(hass: HomeAssistant) -> None:
|
||||
),
|
||||
(ElectricityMapsError("Something else"), "unknown"),
|
||||
(ElectricityMapsConnectionError("Boom"), "unknown"),
|
||||
(ElectricityMapsNoDataError("I have no data"), "no_data"),
|
||||
],
|
||||
ids=[
|
||||
"invalid auth",
|
||||
"generic error",
|
||||
"json decode error",
|
||||
],
|
||||
ids=["invalid auth", "generic error", "json decode error", "no data error"],
|
||||
)
|
||||
async def test_form_error_handling(
|
||||
hass: HomeAssistant,
|
||||
|
@ -3,7 +3,7 @@ from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from deebot_client.const import PATH_API_APPSVR_APP
|
||||
from deebot_client import const
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.exceptions import ApiError
|
||||
from deebot_client.models import Credentials
|
||||
@ -75,9 +75,13 @@ def mock_authenticator(device_fixture: str) -> Generator[Mock, None, None]:
|
||||
query_params: dict[str, Any] | None = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if path == PATH_API_APPSVR_APP:
|
||||
return {"code": 0, "devices": devices, "errno": "0"}
|
||||
raise ApiError("Path not mocked: {path}")
|
||||
match path:
|
||||
case const.PATH_API_APPSVR_APP:
|
||||
return {"code": 0, "devices": devices, "errno": "0"}
|
||||
case const.PATH_API_USERS_USER:
|
||||
return {"todo": "result", "result": "ok", "devices": devices}
|
||||
case _:
|
||||
raise ApiError("Path not mocked: {path}")
|
||||
|
||||
authenticator.post_authenticated.side_effect = post_authenticated
|
||||
yield authenticator
|
||||
|
@ -112,3 +112,14 @@ def mock_router_bridge_mode(mock_device_registry_devices, router):
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_router_bridge_mode_error(mock_device_registry_devices, router):
|
||||
"""Mock a failed connection to Freebox Bridge mode."""
|
||||
|
||||
router().lan.get_hosts_list = AsyncMock(
|
||||
side_effect=HttpRequestError("Request failed (APIResponse: some unknown error)")
|
||||
)
|
||||
|
||||
return router
|
||||
|
@ -69,8 +69,8 @@ async def test_zeroconf(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "link"
|
||||
|
||||
|
||||
async def test_link(hass: HomeAssistant, router: Mock) -> None:
|
||||
"""Test linking."""
|
||||
async def internal_test_link(hass: HomeAssistant) -> None:
|
||||
"""Test linking internal, common to both router modes."""
|
||||
with patch(
|
||||
"homeassistant.components.freebox.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -91,6 +91,30 @@ async def test_link(hass: HomeAssistant, router: Mock) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_link(hass: HomeAssistant, router: Mock) -> None:
|
||||
"""Test link with standard router mode."""
|
||||
await internal_test_link(hass)
|
||||
|
||||
|
||||
async def test_link_bridge_mode(hass: HomeAssistant, router_bridge_mode: Mock) -> None:
|
||||
"""Test linking for a freebox in bridge mode."""
|
||||
await internal_test_link(hass)
|
||||
|
||||
|
||||
async def test_link_bridge_mode_error(
|
||||
hass: HomeAssistant, mock_router_bridge_mode_error: Mock
|
||||
) -> None:
|
||||
"""Test linking for a freebox in bridge mode, unknown error received from API."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_abort_if_already_setup(hass: HomeAssistant) -> None:
|
||||
"""Test we abort if component is already setup."""
|
||||
MockConfigEntry(
|
||||
|
@ -1,7 +1,11 @@
|
||||
"""Tests for the Freebox utility methods."""
|
||||
import json
|
||||
from unittest.mock import Mock
|
||||
|
||||
from homeassistant.components.freebox.router import is_json
|
||||
from freebox_api.exceptions import HttpRequestError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.freebox.router import get_hosts_list_if_supported, is_json
|
||||
|
||||
from .const import DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE, DATA_WIFI_GET_GLOBAL_CONFIG
|
||||
|
||||
@ -20,3 +24,33 @@ async def test_is_json() -> None:
|
||||
assert not is_json("")
|
||||
assert not is_json("XXX")
|
||||
assert not is_json("{XXX}")
|
||||
|
||||
|
||||
async def test_get_hosts_list_if_supported(
|
||||
router: Mock,
|
||||
) -> None:
|
||||
"""In router mode, get_hosts_list is supported and list is filled."""
|
||||
supports_hosts, fbx_devices = await get_hosts_list_if_supported(router())
|
||||
assert supports_hosts is True
|
||||
# List must not be empty; but it's content depends on how many unit tests are executed...
|
||||
assert fbx_devices
|
||||
assert "d633d0c8-958c-43cc-e807-d881b076924b" in str(fbx_devices)
|
||||
|
||||
|
||||
async def test_get_hosts_list_if_supported_bridge(
|
||||
router_bridge_mode: Mock,
|
||||
) -> None:
|
||||
"""In bridge mode, get_hosts_list is NOT supported and list is empty."""
|
||||
supports_hosts, fbx_devices = await get_hosts_list_if_supported(
|
||||
router_bridge_mode()
|
||||
)
|
||||
assert supports_hosts is False
|
||||
assert fbx_devices == []
|
||||
|
||||
|
||||
async def test_get_hosts_list_if_supported_bridge_error(
|
||||
mock_router_bridge_mode_error: Mock,
|
||||
) -> None:
|
||||
"""Other exceptions must be propagated."""
|
||||
with pytest.raises(HttpRequestError):
|
||||
await get_hosts_list_if_supported(mock_router_bridge_mode_error())
|
||||
|
@ -65,54 +65,58 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
async def test_reauth(hass: HomeAssistant) -> None:
|
||||
"""Test reauthentication."""
|
||||
|
||||
entry = await async_init_integration(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_REAUTH,
|
||||
"entry_id": entry.entry_id,
|
||||
"title_placeholders": {"name": entry.title},
|
||||
"unique_id": entry.unique_id,
|
||||
},
|
||||
data=entry.data,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.login",
|
||||
"homeassistant.components.linear_garage_door.async_setup_entry",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.get_sites",
|
||||
return_value=[{"id": "test-site-id", "name": "test-site-name"}],
|
||||
), patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.close",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"uuid.uuid4",
|
||||
return_value="test-uuid",
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"email": "new-email",
|
||||
"password": "new-password",
|
||||
entry = await async_init_integration(hass)
|
||||
|
||||
result1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_REAUTH,
|
||||
"entry_id": entry.entry_id,
|
||||
"title_placeholders": {"name": entry.title},
|
||||
"unique_id": entry.unique_id,
|
||||
},
|
||||
data=entry.data,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result1["type"] == FlowResultType.FORM
|
||||
assert result1["step_id"] == "user"
|
||||
|
||||
assert result2["type"] == FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
with patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.login",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.get_sites",
|
||||
return_value=[{"id": "test-site-id", "name": "test-site-name"}],
|
||||
), patch(
|
||||
"homeassistant.components.linear_garage_door.config_flow.Linear.close",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"uuid.uuid4",
|
||||
return_value="test-uuid",
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result1["flow_id"],
|
||||
{
|
||||
"email": "new-email",
|
||||
"password": "new-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entries = hass.config_entries.async_entries()
|
||||
assert len(entries) == 1
|
||||
assert entries[0].data == {
|
||||
"email": "new-email",
|
||||
"password": "new-password",
|
||||
"site_id": "test-site-id",
|
||||
"device_id": "test-uuid",
|
||||
}
|
||||
assert result2["type"] == FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
entries = hass.config_entries.async_entries()
|
||||
assert len(entries) == 1
|
||||
assert entries[0].data == {
|
||||
"email": "new-email",
|
||||
"password": "new-password",
|
||||
"site_id": "test-site-id",
|
||||
"device_id": "test-uuid",
|
||||
}
|
||||
|
||||
|
||||
async def test_form_invalid_login(hass: HomeAssistant) -> None:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from linear_garage_door.errors import InvalidLoginError, ResponseError
|
||||
from linear_garage_door.errors import InvalidLoginError
|
||||
|
||||
from homeassistant.components.linear_garage_door.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@ -45,32 +45,6 @@ async def test_invalid_password(
|
||||
assert flows[0]["context"]["source"] == "reauth"
|
||||
|
||||
|
||||
async def test_response_error(hass: HomeAssistant) -> None:
|
||||
"""Test response error."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"email": "test-email",
|
||||
"password": "test-password",
|
||||
"site_id": "test-site-id",
|
||||
"device_id": "test-uuid",
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.linear_garage_door.coordinator.Linear.login",
|
||||
side_effect=ResponseError,
|
||||
):
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert entries
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_invalid_login(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
@ -1,15 +1,9 @@
|
||||
"""Fixtures for Met Office weather integration tests."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from datapoint.exceptions import APIException
|
||||
import pytest
|
||||
|
||||
# All tests are marked as disabled, as the integration is disabled in the
|
||||
# integration manifest. `datapoint` isn't compatible with Python 3.12
|
||||
#
|
||||
# from datapoint.exceptions import APIException
|
||||
APIException = Exception
|
||||
collect_ignore_glob = ["test_*.py"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_simple_manager_fail():
|
||||
|
@ -1352,7 +1352,7 @@ async def cover_fixture(
|
||||
suggested_object_id="position_shade",
|
||||
original_name="Position Shade",
|
||||
)
|
||||
cover_position_attributes = {cover.ATTR_POSITION: 50}
|
||||
cover_position_attributes = {cover.ATTR_CURRENT_POSITION: 50}
|
||||
set_state_with_entry(hass, cover_position, STATE_OPEN, cover_position_attributes)
|
||||
data["cover_position"] = cover_position
|
||||
|
||||
@ -1363,7 +1363,7 @@ async def cover_fixture(
|
||||
suggested_object_id="tilt_position_shade",
|
||||
original_name="Tilt Position Shade",
|
||||
)
|
||||
cover_tilt_position_attributes = {cover.ATTR_TILT_POSITION: 50}
|
||||
cover_tilt_position_attributes = {cover.ATTR_CURRENT_TILT_POSITION: 50}
|
||||
set_state_with_entry(
|
||||
hass, cover_tilt_position, STATE_OPEN, cover_tilt_position_attributes
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ async def test_unload_entry(
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert setup_entry.state is ConfigEntryState.LOADED
|
||||
with patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockLocalClient.async_disconnect"
|
||||
"homeassistant.components.roborock.coordinator.RoborockLocalClient.async_release"
|
||||
) as mock_disconnect:
|
||||
assert await hass.config_entries.async_unload(setup_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Test System Monitor sensor."""
|
||||
|
||||
from datetime import timedelta
|
||||
import socket
|
||||
from unittest.mock import Mock, patch
|
||||
@ -429,3 +430,37 @@ async def test_exception_handling_disk_sensor(
|
||||
assert disk_sensor is not None
|
||||
assert disk_sensor.state == "70.0"
|
||||
assert disk_sensor.attributes["unit_of_measurement"] == "%"
|
||||
|
||||
|
||||
async def test_cpu_percentage_is_zero_returns_unknown(
|
||||
hass: HomeAssistant,
|
||||
entity_registry_enabled_by_default: None,
|
||||
mock_psutil: Mock,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the sensor."""
|
||||
cpu_sensor = hass.states.get("sensor.system_monitor_processor_use")
|
||||
assert cpu_sensor is not None
|
||||
assert cpu_sensor.state == "10"
|
||||
|
||||
mock_psutil.cpu_percent.return_value = 0.0
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cpu_sensor = hass.states.get("sensor.system_monitor_processor_use")
|
||||
assert cpu_sensor is not None
|
||||
assert cpu_sensor.state == STATE_UNKNOWN
|
||||
|
||||
mock_psutil.cpu_percent.return_value = 15.0
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cpu_sensor = hass.states.get("sensor.system_monitor_processor_use")
|
||||
assert cpu_sensor is not None
|
||||
assert cpu_sensor.state == "15"
|
||||
|
27
tests/components/technove/fixtures/station_bad_status.json
Normal file
27
tests/components/technove/fixtures/station_bad_status.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"voltageIn": 238,
|
||||
"voltageOut": 238,
|
||||
"maxStationCurrent": 32,
|
||||
"maxCurrent": 24,
|
||||
"current": 23.75,
|
||||
"network_ssid": "Connecting...",
|
||||
"id": "AA:AA:AA:AA:AA:BB",
|
||||
"auto_charge": true,
|
||||
"highChargePeriodActive": false,
|
||||
"normalPeriodActive": false,
|
||||
"maxChargePourcentage": 0.9,
|
||||
"isBatteryProtected": false,
|
||||
"inSharingMode": true,
|
||||
"energySession": 12.34,
|
||||
"energyTotal": 1234,
|
||||
"version": "1.82",
|
||||
"rssi": -82,
|
||||
"name": "TechnoVE Station",
|
||||
"lastCharge": "1701072080,0,17.39\n",
|
||||
"time": 1701000000,
|
||||
"isUpToDate": true,
|
||||
"isSessionActive": true,
|
||||
"conflictInSharingConfig": false,
|
||||
"isStaticIp": false,
|
||||
"status": 12345
|
||||
}
|
@ -297,6 +297,8 @@
|
||||
'unplugged',
|
||||
'plugged_waiting',
|
||||
'plugged_charging',
|
||||
'out_of_activation_period',
|
||||
'high_charge_period',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
@ -333,6 +335,8 @@
|
||||
'unplugged',
|
||||
'plugged_waiting',
|
||||
'plugged_charging',
|
||||
'out_of_activation_period',
|
||||
'high_charge_period',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
@ -5,15 +5,20 @@ from unittest.mock import MagicMock
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
from technove import Status, TechnoVEError
|
||||
from technove import Station, Status, TechnoVEError
|
||||
|
||||
from homeassistant.components.technove.const import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
load_json_object_fixture,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_technove")
|
||||
@ -93,3 +98,27 @@ async def test_sensor_update_failure(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_sensor_unknown_status(
|
||||
hass: HomeAssistant,
|
||||
mock_technove: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator update failure."""
|
||||
entity_id = "sensor.technove_station_status"
|
||||
|
||||
assert hass.states.get(entity_id).state == Status.PLUGGED_CHARGING.value
|
||||
|
||||
mock_technove.update.return_value = Station(
|
||||
load_json_object_fixture("station_bad_status.json", DOMAIN)
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(minutes=5, seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||
# Other sensors should still be available
|
||||
assert hass.states.get("sensor.technove_station_total_energy_usage").state == "1234"
|
||||
|
@ -36,6 +36,7 @@ IP_ADDRESS2 = "127.0.0.2"
|
||||
ALIAS = "My Bulb"
|
||||
MODEL = "HS100"
|
||||
MAC_ADDRESS = "aa:bb:cc:dd:ee:ff"
|
||||
DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "")
|
||||
MAC_ADDRESS2 = "11:22:33:44:55:66"
|
||||
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
||||
CREDENTIALS_HASH_LEGACY = ""
|
||||
|
@ -33,6 +33,7 @@ from . import (
|
||||
DEFAULT_ENTRY_TITLE,
|
||||
DEVICE_CONFIG_DICT_AUTH,
|
||||
DEVICE_CONFIG_DICT_LEGACY,
|
||||
DHCP_FORMATTED_MAC_ADDRESS,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
MAC_ADDRESS2,
|
||||
@ -144,6 +145,7 @@ async def test_discovery_auth(
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == DEFAULT_ENTRY_TITLE
|
||||
assert result2["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result2["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -206,6 +208,7 @@ async def test_discovery_auth_errors(
|
||||
)
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result3["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_discovery_new_credentials(
|
||||
@ -254,6 +257,7 @@ async def test_discovery_new_credentials(
|
||||
)
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result3["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_discovery_new_credentials_invalid(
|
||||
@ -309,6 +313,7 @@ async def test_discovery_new_credentials_invalid(
|
||||
)
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result3["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> None:
|
||||
@ -365,6 +370,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_LEGACY
|
||||
assert result3["context"]["unique_id"] == MAC_ADDRESS
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_setup_entry.assert_called_once()
|
||||
@ -432,6 +438,7 @@ async def test_manual(hass: HomeAssistant) -> None:
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["title"] == DEFAULT_ENTRY_TITLE
|
||||
assert result4["data"] == CREATE_ENTRY_DATA_LEGACY
|
||||
assert result4["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
# Duplicate
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -470,6 +477,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == CREATE_ENTRY_DATA_LEGACY
|
||||
assert result["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_manual_auth(
|
||||
@ -510,6 +518,7 @@ async def test_manual_auth(
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result3["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -572,6 +581,7 @@ async def test_manual_auth_errors(
|
||||
)
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["data"] == CREATE_ENTRY_DATA_AUTH
|
||||
assert result4["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -599,7 +609,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS
|
||||
ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -611,7 +621,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname"
|
||||
ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname"
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -625,7 +635,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname"
|
||||
ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname"
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -638,7 +648,9 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
[
|
||||
(
|
||||
config_entries.SOURCE_DHCP,
|
||||
dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS),
|
||||
dhcp.DhcpServiceInfo(
|
||||
ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS
|
||||
),
|
||||
),
|
||||
(
|
||||
config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
||||
@ -675,6 +687,8 @@ async def test_discovered_by_dhcp_or_discovery(
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["data"] == CREATE_ENTRY_DATA_LEGACY
|
||||
assert result2["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
assert mock_async_setup.called
|
||||
assert mock_async_setup_entry.called
|
||||
|
||||
@ -684,7 +698,9 @@ async def test_discovered_by_dhcp_or_discovery(
|
||||
[
|
||||
(
|
||||
config_entries.SOURCE_DHCP,
|
||||
dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS),
|
||||
dhcp.DhcpServiceInfo(
|
||||
ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS
|
||||
),
|
||||
),
|
||||
(
|
||||
config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
||||
@ -713,7 +729,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_discovery_with_ip_change(
|
||||
async def test_integration_discovery_with_ip_change(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_discovery: AsyncMock,
|
||||
@ -764,6 +780,36 @@ async def test_discovery_with_ip_change(
|
||||
mock_connect["connect"].assert_awaited_once_with(config=config)
|
||||
|
||||
|
||||
async def test_dhcp_discovery_with_ip_change(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test dhcp discovery with an IP change."""
|
||||
mock_connect["connect"].side_effect = SmartDeviceException()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 0
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1"
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip="127.0.0.2", macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS
|
||||
),
|
||||
)
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
|
||||
|
||||
async def test_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
@ -1022,6 +1068,7 @@ async def test_pick_device_errors(
|
||||
},
|
||||
)
|
||||
assert result4["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result4["context"]["unique_id"] == MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_discovery_timeout_connect(
|
||||
@ -1046,6 +1093,7 @@ async def test_discovery_timeout_connect(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["context"]["unique_id"] == MAC_ADDRESS
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
|
||||
|
||||
|
@ -228,6 +228,46 @@ async def test_in_zone_works_for_passive_zones(hass: HomeAssistant) -> None:
|
||||
assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude)
|
||||
|
||||
|
||||
async def test_async_active_zone_with_non_zero_radius(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_active_zone with a non-zero radius."""
|
||||
latitude = 32.880600
|
||||
longitude = -117.237561
|
||||
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
zone.DOMAIN,
|
||||
{
|
||||
"zone": [
|
||||
{
|
||||
"name": "Small Zone",
|
||||
"latitude": 32.980600,
|
||||
"longitude": -117.137561,
|
||||
"radius": 50000,
|
||||
},
|
||||
{
|
||||
"name": "Big Zone",
|
||||
"latitude": 32.980600,
|
||||
"longitude": -117.137561,
|
||||
"radius": 100000,
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
home_state = hass.states.get("zone.home")
|
||||
assert home_state.attributes["radius"] == 100
|
||||
assert home_state.attributes["latitude"] == 32.87336
|
||||
assert home_state.attributes["longitude"] == -117.22743
|
||||
|
||||
active = zone.async_active_zone(hass, latitude, longitude, 5000)
|
||||
assert active.entity_id == "zone.home"
|
||||
|
||||
active = zone.async_active_zone(hass, latitude, longitude, 0)
|
||||
assert active.entity_id == "zone.small_zone"
|
||||
|
||||
|
||||
async def test_core_config_update(hass: HomeAssistant) -> None:
|
||||
"""Test updating core config will update home zone."""
|
||||
assert await setup.async_setup_component(hass, "zone", {})
|
||||
|
Loading…
x
Reference in New Issue
Block a user