This commit is contained in:
Franck Nijhof 2024-02-16 15:47:38 +01:00 committed by GitHub
commit 7aa14e20d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 542 additions and 276 deletions

View File

@ -249,8 +249,9 @@ 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)

View File

@ -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"]
}

View File

@ -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:

View File

@ -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%]"
}
},

View File

@ -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",

View File

@ -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",

View File

@ -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"]
}

View File

@ -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
def _create_elk_services(hass: HomeAssistant) -> None:
def _getelk(service: ServiceCall) -> Elk:
@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
return elk.panel
def _create_elk_services(hass: HomeAssistant) -> None:
"""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

View File

@ -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"]
}

View File

@ -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()

View File

@ -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)
self.supports_hosts, fbx_devices = await get_hosts_list_if_supported(
self._api
)
)
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
# Adds the Freebox itself
fbx_devices.append(

View File

@ -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

View File

@ -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."
}
}
}

View File

@ -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"
]
}

View File

@ -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)

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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]

View File

@ -2,9 +2,7 @@
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

View File

@ -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

View File

@ -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"]
}

View File

@ -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,
}

View File

@ -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."""

View File

@ -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()

View File

@ -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

View File

@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aiopegelonline"],
"requirements": ["aiopegelonline==0.0.6"]
"requirements": ["aiopegelonline==0.0.8"]
}

View File

@ -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",

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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."""

View File

@ -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:

View File

@ -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:

View File

@ -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": [
{

View File

@ -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"]

View File

@ -117,7 +117,7 @@
"speed": {
"default": "mdi:speedometer"
},
"sulfur_dioxide": {
"sulphur_dioxide": {
"default": "mdi:molecule"
},
"temperature": {

View File

@ -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"]
}

View File

@ -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

View File

@ -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"]
}

View File

@ -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]):

View File

@ -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]]

View File

@ -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."]
}

View File

@ -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"
}
}
}

View File

@ -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(

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==70"],
"requirements": ["aiounifi==71"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -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"

View File

@ -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"]
}

View File

@ -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.",

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -4,5 +4,4 @@ ARG \
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs
raspberrypi-utils

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,8 +75,12 @@ 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:
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

View File

@ -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

View File

@ -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(

View File

@ -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())

View File

@ -65,9 +65,13 @@ async def test_form(hass: HomeAssistant) -> None:
async def test_reauth(hass: HomeAssistant) -> None:
"""Test reauthentication."""
with patch(
"homeassistant.components.linear_garage_door.async_setup_entry",
return_value=True,
):
entry = await async_init_integration(hass)
result = await hass.config_entries.flow.async_init(
result1 = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
@ -77,8 +81,8 @@ async def test_reauth(hass: HomeAssistant) -> None:
},
data=entry.data,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result1["type"] == FlowResultType.FORM
assert result1["step_id"] == "user"
with patch(
"homeassistant.components.linear_garage_door.config_flow.Linear.login",
@ -94,7 +98,7 @@ async def test_reauth(hass: HomeAssistant) -> None:
return_value="test-uuid",
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
result1["flow_id"],
{
"email": "new-email",
"password": "new-password",

View File

@ -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:

View File

@ -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():

View File

@ -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
)

View File

@ -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()

View File

@ -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"

View 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
}

View File

@ -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>,

View File

@ -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"

View File

@ -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 = ""

View File

@ -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

View File

@ -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", {})