From 743308ec92f1ed2c7a5cc56d6552c4adbe08cae9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Jul 2021 21:26:51 +0200 Subject: [PATCH 001/199] Bumped version to 2021.8.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0b8523bfa6f..69a06c04f02 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From a3a687f03741a284b7dfc257c3f2522801381a37 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 28 Jul 2021 21:41:11 +0200 Subject: [PATCH 002/199] Add renault integration (#39605) --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/renault/__init__.py | 45 +++ .../components/renault/config_flow.py | 92 +++++ homeassistant/components/renault/const.py | 15 + .../components/renault/manifest.json | 13 + .../components/renault/renault_coordinator.py | 72 ++++ .../components/renault/renault_entities.py | 103 ++++++ .../components/renault/renault_hub.py | 78 +++++ .../components/renault/renault_vehicle.py | 146 ++++++++ homeassistant/components/renault/sensor.py | 277 +++++++++++++++ homeassistant/components/renault/strings.json | 27 ++ .../components/renault/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/renault/__init__.py | 159 +++++++++ tests/components/renault/const.py | 328 ++++++++++++++++++ tests/components/renault/test_config_flow.py | 137 ++++++++ tests/components/renault/test_init.py | 85 +++++ tests/components/renault/test_sensor.py | 212 +++++++++++ .../renault/battery_status_charging.json | 18 + .../renault/battery_status_not_charging.json | 15 + .../fixtures/renault/charge_mode_always.json | 7 + .../renault/charge_mode_schedule.json | 7 + tests/fixtures/renault/cockpit_ev.json | 9 + tests/fixtures/renault/cockpit_fuel.json | 11 + tests/fixtures/renault/hvac_status.json | 7 + .../fixtures/renault/vehicle_captur_fuel.json | 108 ++++++ .../fixtures/renault/vehicle_captur_phev.json | 110 ++++++ tests/fixtures/renault/vehicle_zoe_40.json | 189 ++++++++++ tests/fixtures/renault/vehicle_zoe_50.json | 161 +++++++++ 33 files changed, 2478 insertions(+) create mode 100644 homeassistant/components/renault/__init__.py create mode 100644 homeassistant/components/renault/config_flow.py create mode 100644 homeassistant/components/renault/const.py create mode 100644 homeassistant/components/renault/manifest.json create mode 100644 homeassistant/components/renault/renault_coordinator.py create mode 100644 homeassistant/components/renault/renault_entities.py create mode 100644 homeassistant/components/renault/renault_hub.py create mode 100644 homeassistant/components/renault/renault_vehicle.py create mode 100644 homeassistant/components/renault/sensor.py create mode 100644 homeassistant/components/renault/strings.json create mode 100644 homeassistant/components/renault/translations/en.json create mode 100644 tests/components/renault/__init__.py create mode 100644 tests/components/renault/const.py create mode 100644 tests/components/renault/test_config_flow.py create mode 100644 tests/components/renault/test_init.py create mode 100644 tests/components/renault/test_sensor.py create mode 100644 tests/fixtures/renault/battery_status_charging.json create mode 100644 tests/fixtures/renault/battery_status_not_charging.json create mode 100644 tests/fixtures/renault/charge_mode_always.json create mode 100644 tests/fixtures/renault/charge_mode_schedule.json create mode 100644 tests/fixtures/renault/cockpit_ev.json create mode 100644 tests/fixtures/renault/cockpit_fuel.json create mode 100644 tests/fixtures/renault/hvac_status.json create mode 100644 tests/fixtures/renault/vehicle_captur_fuel.json create mode 100644 tests/fixtures/renault/vehicle_captur_phev.json create mode 100644 tests/fixtures/renault/vehicle_zoe_40.json create mode 100644 tests/fixtures/renault/vehicle_zoe_50.json diff --git a/.strict-typing b/.strict-typing index e32af5db563..6066c158b99 100644 --- a/.strict-typing +++ b/.strict-typing @@ -81,6 +81,7 @@ homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.remote.* +homeassistant.components.renault.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.scene.* homeassistant.components.select.* diff --git a/CODEOWNERS b/CODEOWNERS index bfd74e97f71..c4cb6d242d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/recollect_waste/* @bachya homeassistant/components/rejseplanen/* @DarkFox +homeassistant/components/renault/* @epenet homeassistant/components/repetier/* @MTrab homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py new file mode 100644 index 00000000000..80433b2106e --- /dev/null +++ b/homeassistant/components/renault/__init__.py @@ -0,0 +1,45 @@ +"""Support for Renault devices.""" +import aiohttp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_LOCALE, DOMAIN, PLATFORMS +from .renault_hub import RenaultHub + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Load a config entry.""" + renault_hub = RenaultHub(hass, config_entry.data[CONF_LOCALE]) + try: + login_success = await renault_hub.attempt_login( + config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + ) + except aiohttp.ClientConnectionError as exc: + raise ConfigEntryNotReady() from exc + + if not login_success: + return False + + hass.data.setdefault(DOMAIN, {}) + await renault_hub.async_initialise(config_entry) + + hass.data[DOMAIN][config_entry.unique_id] = renault_hub + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.unique_id) + + return unload_ok diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py new file mode 100644 index 00000000000..09a69f1f95f --- /dev/null +++ b/homeassistant/components/renault/config_flow.py @@ -0,0 +1,92 @@ +"""Config flow to configure Renault component.""" +from __future__ import annotations + +from typing import Any + +from renault_api.const import AVAILABLE_LOCALES +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, DOMAIN +from .renault_hub import RenaultHub + + +class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Renault config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the Renault config flow.""" + self.renault_config: dict[str, Any] = {} + self.renault_hub: RenaultHub | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a Renault config flow start. + + Ask the user for API keys. + """ + if user_input: + locale = user_input[CONF_LOCALE] + self.renault_config.update(user_input) + self.renault_config.update(AVAILABLE_LOCALES[locale]) + self.renault_hub = RenaultHub(self.hass, locale) + if not await self.renault_hub.attempt_login( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ): + return self._show_user_form({"base": "invalid_credentials"}) + return await self.async_step_kamereon() + return self._show_user_form() + + def _show_user_form(self, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the API keys form.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_LOCALE): vol.In(AVAILABLE_LOCALES.keys()), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors or {}, + ) + + async def async_step_kamereon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select Kamereon account.""" + if user_input: + await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID]) + self._abort_if_unique_id_configured() + + self.renault_config.update(user_input) + return self.async_create_entry( + title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config + ) + + assert self.renault_hub + accounts = await self.renault_hub.get_account_ids() + if len(accounts) == 0: + return self.async_abort(reason="kamereon_no_account") + if len(accounts) == 1: + await self.async_set_unique_id(accounts[0]) + self._abort_if_unique_id_configured() + + self.renault_config[CONF_KAMEREON_ACCOUNT_ID] = accounts[0] + return self.async_create_entry( + title=self.renault_config[CONF_KAMEREON_ACCOUNT_ID], + data=self.renault_config, + ) + + return self.async_show_form( + step_id="kamereon", + data_schema=vol.Schema( + {vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)} + ), + ) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py new file mode 100644 index 00000000000..51f6c10c6f1 --- /dev/null +++ b/homeassistant/components/renault/const.py @@ -0,0 +1,15 @@ +"""Constants for the Renault component.""" +DOMAIN = "renault" + +CONF_LOCALE = "locale" +CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" + +DEFAULT_SCAN_INTERVAL = 300 # 5 minutes + +PLATFORMS = [ + "sensor", +] + +DEVICE_CLASS_PLUG_STATE = "renault__plug_state" +DEVICE_CLASS_CHARGE_STATE = "renault__charge_state" +DEVICE_CLASS_CHARGE_MODE = "renault__charge_mode" diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json new file mode 100644 index 00000000000..118848ad6dd --- /dev/null +++ b/homeassistant/components/renault/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "renault", + "name": "Renault", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/renault", + "requirements": [ + "renault-api==0.1.4" + ], + "codeowners": [ + "@epenet" + ], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py new file mode 100644 index 00000000000..b47a8507030 --- /dev/null +++ b/homeassistant/components/renault/renault_coordinator.py @@ -0,0 +1,72 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +from collections.abc import Awaitable +from datetime import timedelta +import logging +from typing import Callable, TypeVar + +from renault_api.kamereon.exceptions import ( + AccessDeniedException, + KamereonResponseException, + NotSupportedException, +) + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +T = TypeVar("T") + + +class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): + """Handle vehicle communication with Renault servers.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: timedelta, + update_method: Callable[[], Awaitable[T]], + ) -> None: + """Initialise coordinator.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + update_method=update_method, + ) + self.access_denied = False + self.not_supported = False + + async def _async_update_data(self) -> T: + """Fetch the latest data from the source.""" + if self.update_method is None: + raise NotImplementedError("Update method not implemented") + try: + return await self.update_method() + except AccessDeniedException as err: + # Disable because the account is not allowed to access this Renault endpoint. + self.update_interval = None + self.access_denied = True + raise UpdateFailed(f"This endpoint is denied: {err}") from err + + except NotSupportedException as err: + # Disable because the vehicle does not support this Renault endpoint. + self.update_interval = None + self.not_supported = True + raise UpdateFailed(f"This endpoint is not supported: {err}") from err + + except KamereonResponseException as err: + # Other Renault errors. + raise UpdateFailed(f"Error communicating with API: {err}") from err + + async def async_config_entry_first_refresh(self) -> None: + """Refresh data for the first time when a config entry is setup. + + Contrary to base implementation, we are not raising ConfigEntryNotReady + but only updating the `access_denied` and `not_supported` flags. + """ + await self._async_refresh(log_failures=False, raise_on_auth_failed=True) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py new file mode 100644 index 00000000000..9188a1f0757 --- /dev/null +++ b/homeassistant/components/renault/renault_entities.py @@ -0,0 +1,103 @@ +"""Base classes for Renault entities.""" +from __future__ import annotations + +from typing import Any, Generic, Optional, TypeVar + +from renault_api.kamereon.enums import ChargeState, PlugState +from renault_api.kamereon.models import ( + KamereonVehicleBatteryStatusData, + KamereonVehicleChargeModeData, + KamereonVehicleCockpitData, + KamereonVehicleHvacStatusData, +) + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify + +from .renault_vehicle import RenaultVehicleProxy + +ATTR_LAST_UPDATE = "last_update" + +T = TypeVar("T") + + +class RenaultDataEntity(Generic[T], CoordinatorEntity[Optional[T]], Entity): + """Implementation of a Renault entity with a data coordinator.""" + + def __init__( + self, vehicle: RenaultVehicleProxy, entity_type: str, coordinator_key: str + ) -> None: + """Initialise entity.""" + super().__init__(vehicle.coordinators[coordinator_key]) + self.vehicle = vehicle + self._entity_type = entity_type + self._attr_device_info = self.vehicle.device_info + self._attr_name = entity_type + self._attr_unique_id = slugify( + f"{self.vehicle.details.vin}-{self._entity_type}" + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + # Data can succeed, but be empty + return super().available and self.coordinator.data is not None + + @property + def data(self) -> T | None: + """Return collected data.""" + return self.coordinator.data + + +class RenaultBatteryDataEntity(RenaultDataEntity[KamereonVehicleBatteryStatusData]): + """Implementation of a Renault entity with battery coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "battery") + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of this entity.""" + last_update = self.data.timestamp if self.data else None + return {ATTR_LAST_UPDATE: last_update} + + @property + def is_charging(self) -> bool: + """Return charge state as boolean.""" + return ( + self.data is not None + and self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS + ) + + @property + def is_plugged_in(self) -> bool: + """Return plug state as boolean.""" + return ( + self.data is not None and self.data.get_plug_status() == PlugState.PLUGGED + ) + + +class RenaultChargeModeDataEntity(RenaultDataEntity[KamereonVehicleChargeModeData]): + """Implementation of a Renault entity with charge_mode coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "charge_mode") + + +class RenaultCockpitDataEntity(RenaultDataEntity[KamereonVehicleCockpitData]): + """Implementation of a Renault entity with cockpit coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "cockpit") + + +class RenaultHVACDataEntity(RenaultDataEntity[KamereonVehicleHvacStatusData]): + """Implementation of a Renault entity with hvac_status coordinator.""" + + def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: + """Initialise entity.""" + super().__init__(vehicle, entity_type, "hvac_status") diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py new file mode 100644 index 00000000000..51e356934bb --- /dev/null +++ b/homeassistant/components/renault/renault_hub.py @@ -0,0 +1,78 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.renault_account import RenaultAccount +from renault_api.renault_client import RenaultClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_KAMEREON_ACCOUNT_ID, DEFAULT_SCAN_INTERVAL +from .renault_vehicle import RenaultVehicleProxy + +LOGGER = logging.getLogger(__name__) + + +class RenaultHub: + """Handle account communication with Renault servers.""" + + def __init__(self, hass: HomeAssistant, locale: str) -> None: + """Initialise proxy.""" + LOGGER.debug("Creating RenaultHub") + self._hass = hass + self._client = RenaultClient( + websession=async_get_clientsession(self._hass), locale=locale + ) + self._account: RenaultAccount | None = None + self._vehicles: dict[str, RenaultVehicleProxy] = {} + + async def attempt_login(self, username: str, password: str) -> bool: + """Attempt login to Renault servers.""" + try: + await self._client.session.login(username, password) + except InvalidCredentialsException as ex: + LOGGER.error("Login to Renault failed: %s", ex.error_details) + else: + return True + return False + + async def async_initialise(self, config_entry: ConfigEntry) -> None: + """Set up proxy.""" + account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID] + scan_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL) + + self._account = await self._client.get_api_account(account_id) + vehicles = await self._account.get_vehicles() + if vehicles.vehicleLinks: + for vehicle_link in vehicles.vehicleLinks: + if vehicle_link.vin and vehicle_link.vehicleDetails: + # Generate vehicle proxy + vehicle = RenaultVehicleProxy( + hass=self._hass, + vehicle=await self._account.get_api_vehicle(vehicle_link.vin), + details=vehicle_link.vehicleDetails, + scan_interval=scan_interval, + ) + await vehicle.async_initialise() + self._vehicles[vehicle_link.vin] = vehicle + + async def get_account_ids(self) -> list[str]: + """Get Kamereon account ids.""" + accounts = [] + for account in await self._client.get_api_accounts(): + vehicles = await account.get_vehicles() + + # Only add the account if it has linked vehicles. + if vehicles.vehicleLinks: + accounts.append(account.account_id) + return accounts + + @property + def vehicles(self) -> dict[str, RenaultVehicleProxy]: + """Get list of vehicles.""" + return self._vehicles diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py new file mode 100644 index 00000000000..09e3de9adab --- /dev/null +++ b/homeassistant/components/renault/renault_vehicle.py @@ -0,0 +1,146 @@ +"""Proxy to handle account communication with Renault servers.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +from typing import cast + +from renault_api.kamereon import models +from renault_api.renault_vehicle import RenaultVehicle + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN +from .renault_coordinator import RenaultDataUpdateCoordinator + +LOGGER = logging.getLogger(__name__) + + +class RenaultVehicleProxy: + """Handle vehicle communication with Renault servers.""" + + def __init__( + self, + hass: HomeAssistant, + vehicle: RenaultVehicle, + details: models.KamereonVehicleDetails, + scan_interval: timedelta, + ) -> None: + """Initialise vehicle proxy.""" + self.hass = hass + self._vehicle = vehicle + self._details = details + self._device_info: DeviceInfo = { + "identifiers": {(DOMAIN, cast(str, details.vin))}, + "manufacturer": (details.get_brand_label() or "").capitalize(), + "model": (details.get_model_label() or "").capitalize(), + "name": details.registrationNumber or "", + "sw_version": details.get_model_code() or "", + } + self.coordinators: dict[str, RenaultDataUpdateCoordinator] = {} + self.hvac_target_temperature = 21 + self._scan_interval = scan_interval + + @property + def details(self) -> models.KamereonVehicleDetails: + """Return the specs of the vehicle.""" + return self._details + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + return self._device_info + + async def async_initialise(self) -> None: + """Load available sensors.""" + if await self.endpoint_available("cockpit"): + self.coordinators["cockpit"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} cockpit", + update_method=self.get_cockpit, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if await self.endpoint_available("hvac-status"): + self.coordinators["hvac_status"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} hvac_status", + update_method=self.get_hvac_status, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if self.details.uses_electricity(): + if await self.endpoint_available("battery-status"): + self.coordinators["battery"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} battery", + update_method=self.get_battery_status, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + if await self.endpoint_available("charge-mode"): + self.coordinators["charge_mode"] = RenaultDataUpdateCoordinator( + self.hass, + LOGGER, + # Name of the data. For logging purposes. + name=f"{self.details.vin} charge_mode", + update_method=self.get_charge_mode, + # Polling interval. Will only be polled if there are subscribers. + update_interval=self._scan_interval, + ) + # Check all coordinators + await asyncio.gather( + *( + coordinator.async_config_entry_first_refresh() + for coordinator in self.coordinators.values() + ) + ) + for key in list(self.coordinators): + # list() to avoid Runtime iteration error + coordinator = self.coordinators[key] + if coordinator.not_supported: + # Remove endpoint as it is not supported for this vehicle. + LOGGER.error( + "Ignoring endpoint %s as it is not supported for this vehicle: %s", + coordinator.name, + coordinator.last_exception, + ) + del self.coordinators[key] + elif coordinator.access_denied: + # Remove endpoint as it is denied for this vehicle. + LOGGER.error( + "Ignoring endpoint %s as it is denied for this vehicle: %s", + coordinator.name, + coordinator.last_exception, + ) + del self.coordinators[key] + + async def endpoint_available(self, endpoint: str) -> bool: + """Ensure the endpoint is available to avoid unnecessary queries.""" + return await self._vehicle.supports_endpoint( + endpoint + ) and await self._vehicle.has_contract_for_endpoint(endpoint) + + async def get_battery_status(self) -> models.KamereonVehicleBatteryStatusData: + """Get battery status information from vehicle.""" + return await self._vehicle.get_battery_status() + + async def get_charge_mode(self) -> models.KamereonVehicleChargeModeData: + """Get charge mode information from vehicle.""" + return await self._vehicle.get_charge_mode() + + async def get_cockpit(self) -> models.KamereonVehicleCockpitData: + """Get cockpit information from vehicle.""" + return await self._vehicle.get_cockpit() + + async def get_hvac_status(self) -> models.KamereonVehicleHvacStatusData: + """Get hvac status information from vehicle.""" + return await self._vehicle.get_hvac_status() diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py new file mode 100644 index 00000000000..8403a04d001 --- /dev/null +++ b/homeassistant/components/renault/sensor.py @@ -0,0 +1,277 @@ +"""Support for Renault sensors.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + TEMP_CELSIUS, + TIME_MINUTES, + VOLUME_LITERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util import slugify + +from .const import ( + DEVICE_CLASS_CHARGE_MODE, + DEVICE_CLASS_CHARGE_STATE, + DEVICE_CLASS_PLUG_STATE, + DOMAIN, +) +from .renault_entities import ( + RenaultBatteryDataEntity, + RenaultChargeModeDataEntity, + RenaultCockpitDataEntity, + RenaultDataEntity, + RenaultHVACDataEntity, +) +from .renault_hub import RenaultHub +from .renault_vehicle import RenaultVehicleProxy + +ATTR_BATTERY_AVAILABLE_ENERGY = "battery_available_energy" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Renault entities from config entry.""" + proxy: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] + entities = await get_entities(proxy) + async_add_entities(entities) + + +async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: + """Create Renault entities for all vehicles.""" + entities = [] + for vehicle in proxy.vehicles.values(): + entities.extend(await get_vehicle_entities(vehicle)) + return entities + + +async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: + """Create Renault entities for single vehicle.""" + entities: list[RenaultDataEntity] = [] + if "cockpit" in vehicle.coordinators: + entities.append(RenaultMileageSensor(vehicle, "Mileage")) + if vehicle.details.uses_fuel(): + entities.append(RenaultFuelAutonomySensor(vehicle, "Fuel Autonomy")) + entities.append(RenaultFuelQuantitySensor(vehicle, "Fuel Quantity")) + if "hvac_status" in vehicle.coordinators: + entities.append(RenaultOutsideTemperatureSensor(vehicle, "Outside Temperature")) + if "battery" in vehicle.coordinators: + entities.append(RenaultBatteryLevelSensor(vehicle, "Battery Level")) + entities.append(RenaultChargeStateSensor(vehicle, "Charge State")) + entities.append( + RenaultChargingRemainingTimeSensor(vehicle, "Charging Remaining Time") + ) + entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power")) + entities.append(RenaultPlugStateSensor(vehicle, "Plug State")) + entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy")) + entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature")) + if "charge_mode" in vehicle.coordinators: + entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode")) + return entities + + +class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): + """Battery autonomy sensor.""" + + _attr_icon = "mdi:ev-station" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryAutonomy if self.data else None + + +class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): + """Battery Level sensor.""" + + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryLevel if self.data else None + + @property + def icon(self) -> str: + """Icon handling.""" + return icon_for_battery_level( + battery_level=self.state, charging=self.is_charging + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of this entity.""" + attrs = super().extra_state_attributes + attrs[ATTR_BATTERY_AVAILABLE_ENERGY] = ( + self.data.batteryAvailableEnergy if self.data else None + ) + return attrs + + +class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity): + """Battery Temperature sensor.""" + + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.batteryTemperature if self.data else None + + +class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity): + """Charge Mode sensor.""" + + _attr_device_class = DEVICE_CLASS_CHARGE_MODE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + return self.data.chargeMode if self.data else None + + @property + def icon(self) -> str: + """Icon handling.""" + if self.data and self.data.chargeMode == "schedule_mode": + return "mdi:calendar-clock" + return "mdi:calendar-remove" + + +class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): + """Charge State sensor.""" + + _attr_device_class = DEVICE_CLASS_CHARGE_STATE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + charging_status = self.data.get_charging_status() if self.data else None + return slugify(charging_status.name) if charging_status is not None else None + + @property + def icon(self) -> str: + """Icon handling.""" + return "mdi:flash" if self.is_charging else "mdi:flash-off" + + +class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity): + """Charging Remaining Time sensor.""" + + _attr_icon = "mdi:timer" + _attr_unit_of_measurement = TIME_MINUTES + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return self.data.chargingRemainingTime if self.data else None + + +class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): + """Charging Power sensor.""" + + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_unit_of_measurement = POWER_KILO_WATT + + @property + def state(self) -> float | None: + """Return the state of this entity.""" + if not self.data or self.data.chargingInstantaneousPower is None: + return None + if self.vehicle.details.reports_charging_power_in_watts(): + # Need to convert to kilowatts + return self.data.chargingInstantaneousPower / 1000 + return self.data.chargingInstantaneousPower + + +class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): + """Fuel autonomy sensor.""" + + _attr_icon = "mdi:gas-station" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.fuelAutonomy) + if self.data and self.data.fuelAutonomy is not None + else None + ) + + +class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): + """Fuel quantity sensor.""" + + _attr_icon = "mdi:fuel" + _attr_unit_of_measurement = VOLUME_LITERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.fuelQuantity) + if self.data and self.data.fuelQuantity is not None + else None + ) + + +class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): + """Mileage sensor.""" + + _attr_icon = "mdi:sign-direction" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + @property + def state(self) -> int | None: + """Return the state of this entity.""" + return ( + round(self.data.totalMileage) + if self.data and self.data.totalMileage is not None + else None + ) + + +class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): + """HVAC Outside Temperature sensor.""" + + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + + @property + def state(self) -> float | None: + """Return the state of this entity.""" + return self.data.externalTemperature if self.data else None + + +class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): + """Plug State sensor.""" + + _attr_device_class = DEVICE_CLASS_PLUG_STATE + + @property + def state(self) -> str | None: + """Return the state of this entity.""" + plug_status = self.data.get_plug_status() if self.data else None + return slugify(plug_status.name) if plug_status is not None else None + + @property + def icon(self) -> str: + """Icon handling.""" + return "mdi:power-plug" if self.is_plugged_in else "mdi:power-plug-off" diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json new file mode 100644 index 00000000000..942c8b4a06c --- /dev/null +++ b/homeassistant/components/renault/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "kamereon_no_account": "Unable to find Kamereon account." + }, + "error": { + "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "Set Renault credentials" + } + } + } +} diff --git a/homeassistant/components/renault/translations/en.json b/homeassistant/components/renault/translations/en.json new file mode 100644 index 00000000000..bb65493a3b3 --- /dev/null +++ b/homeassistant/components/renault/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Account already configured", + "kamereon_no_account": "Unable to find Kamereon account." + }, + "error": { + "invalid_credentials": "Invalid credentials." + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "username": "Email", + "password": "Password" + }, + "title": "Set Renault credentials" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a6c97cbbab3..89b1a9ba8ae 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -216,6 +216,7 @@ FLOWS = [ "rachio", "rainmachine", "recollect_waste", + "renault", "rfxtrx", "ring", "risco", diff --git a/mypy.ini b/mypy.ini index 32800994e42..e38897bf303 100644 --- a/mypy.ini +++ b/mypy.ini @@ -902,6 +902,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.renault.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 0acdff35fb9..bfa61df2f9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2015,6 +2015,9 @@ raspyrfm-client==1.2.8 # homeassistant.components.rainmachine regenmaschine==3.1.5 +# homeassistant.components.renault +renault-api==0.1.4 + # homeassistant.components.python_script restrictedpython==5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b9513a7e54..7d2e39327df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1109,6 +1109,9 @@ rachiopy==1.0.3 # homeassistant.components.rainmachine regenmaschine==3.1.5 +# homeassistant.components.renault +renault-api==0.1.4 + # homeassistant.components.python_script restrictedpython==5.1 diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py new file mode 100644 index 00000000000..e4edc3b8539 --- /dev/null +++ b/tests/components/renault/__init__.py @@ -0,0 +1,159 @@ +"""Tests for the Renault integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any +from unittest.mock import patch + +from renault_api.kamereon import models, schemas +from renault_api.renault_vehicle import RenaultVehicle + +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DOMAIN, +) +from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from .const import MOCK_VEHICLES + +from tests.common import MockConfigEntry, load_fixture + + +async def setup_renault_integration(hass: HomeAssistant): + """Create the Renault integration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source="user", + data={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + CONF_KAMEREON_ACCOUNT_ID: "account_id_2", + }, + unique_id="account_id_2", + options={}, + entry_id="1", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.renault.RenaultHub.attempt_login", return_value=True + ), patch("homeassistant.components.renault.RenaultHub.async_initialise"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +def get_fixtures(vehicle_type: str) -> dict[str, Any]: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + return { + "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") + if "battery_status" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema), + "charge_mode": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}") + if "charge_mode" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), + "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") + if "cockpit" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleCockpitDataSchema), + "hvac_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}") + if "hvac_status" in mock_vehicle["endpoints"] + else "{}" + ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), + } + + +async def create_vehicle_proxy( + hass: HomeAssistant, vehicle_type: str +) -> RenaultVehicleProxy: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + mock_fixtures = get_fixtures(vehicle_type) + + vehicles_response: models.KamereonVehiclesResponse = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ) + vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails + vehicle = RenaultVehicle( + vehicles_response.accountId, + vehicle_details.vin, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + vehicle_proxy = RenaultVehicleProxy( + hass, vehicle, vehicle_details, timedelta(seconds=300) + ) + with patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ): + await vehicle_proxy.async_initialise() + return vehicle_proxy + + +async def create_vehicle_proxy_with_side_effect( + hass: HomeAssistant, vehicle_type: str, side_effect: Any +) -> RenaultVehicleProxy: + """Create a vehicle proxy for testing unavailable entities.""" + mock_vehicle = MOCK_VEHICLES[vehicle_type] + + vehicles_response: models.KamereonVehiclesResponse = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ) + vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails + vehicle = RenaultVehicle( + vehicles_response.accountId, + vehicle_details.vin, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + vehicle_proxy = RenaultVehicleProxy( + hass, vehicle, vehicle_details, timedelta(seconds=300) + ) + with patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + side_effect=side_effect, + ), patch( + "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + side_effect=side_effect, + ): + await vehicle_proxy.async_initialise() + return vehicle_proxy diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py new file mode 100644 index 00000000000..be2adafd7be --- /dev/null +++ b/tests/components/renault/const.py @@ -0,0 +1,328 @@ +"""Constants for the Renault integration tests.""" +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DEVICE_CLASS_CHARGE_MODE, + DEVICE_CLASS_CHARGE_STATE, + DEVICE_CLASS_PLUG_STATE, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, + TIME_MINUTES, + VOLUME_LITERS, +) + +# Mock config data to be used across multiple tests +MOCK_CONFIG = { + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + CONF_KAMEREON_ACCOUNT_ID: "account_id_1", + CONF_LOCALE: "fr_FR", +} + +MOCK_VEHICLES = { + "zoe_40": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, + "manufacturer": "Renault", + "model": "Zoe", + "name": "REG-NUMBER", + "sw_version": "X101VE", + }, + "endpoints_available": [ + True, # cockpit + True, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_charging.json", + "charge_mode": "charge_mode_always.json", + "cockpit": "cockpit_ev.json", + "hvac_status": "hvac_status.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777999_battery_autonomy", + "result": "141", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777999_battery_level", + "result": "60", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777999_battery_temperature", + "result": "20", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777999_charge_mode", + "result": "always", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777999_charge_state", + "result": "charge_in_progress", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777999_charging_power", + "result": "0.027", + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777999_charging_remaining_time", + "result": "145", + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777999_mileage", + "result": "49114", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.outside_temperature", + "unique_id": "vf1aaaaa555777999_outside_temperature", + "result": "8.0", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777999_plug_state", + "result": "plugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "zoe_50": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, + "manufacturer": "Renault", + "model": "Zoe", + "name": "REG-NUMBER", + "sw_version": "X102VE", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_not_charging.json", + "charge_mode": "charge_mode_schedule.json", + "cockpit": "cockpit_ev.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777999_battery_autonomy", + "result": "128", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777999_battery_level", + "result": "50", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777999_battery_temperature", + "result": STATE_UNKNOWN, + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777999_charge_mode", + "result": "schedule_mode", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777999_charge_state", + "result": "charge_error", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777999_charging_power", + "result": STATE_UNKNOWN, + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777999_charging_remaining_time", + "result": STATE_UNKNOWN, + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777999_mileage", + "result": "49114", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777999_plug_state", + "result": "unplugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "captur_phev": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, + "manufacturer": "Renault", + "model": "Captur ii", + "name": "REG-NUMBER", + "sw_version": "XJB1SU", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + True, # battery-status + True, # charge-mode + ], + "endpoints": { + "battery_status": "battery_status_charging.json", + "charge_mode": "charge_mode_always.json", + "cockpit": "cockpit_fuel.json", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.battery_autonomy", + "unique_id": "vf1aaaaa555777123_battery_autonomy", + "result": "141", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.battery_level", + "unique_id": "vf1aaaaa555777123_battery_level", + "result": "60", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_BATTERY, + }, + { + "entity_id": "sensor.battery_temperature", + "unique_id": "vf1aaaaa555777123_battery_temperature", + "result": "20", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.charge_mode", + "unique_id": "vf1aaaaa555777123_charge_mode", + "result": "always", + "class": DEVICE_CLASS_CHARGE_MODE, + }, + { + "entity_id": "sensor.charge_state", + "unique_id": "vf1aaaaa555777123_charge_state", + "result": "charge_in_progress", + "class": DEVICE_CLASS_CHARGE_STATE, + }, + { + "entity_id": "sensor.charging_power", + "unique_id": "vf1aaaaa555777123_charging_power", + "result": "27.0", + "unit": POWER_KILO_WATT, + "class": DEVICE_CLASS_ENERGY, + }, + { + "entity_id": "sensor.charging_remaining_time", + "unique_id": "vf1aaaaa555777123_charging_remaining_time", + "result": "145", + "unit": TIME_MINUTES, + }, + { + "entity_id": "sensor.fuel_autonomy", + "unique_id": "vf1aaaaa555777123_fuel_autonomy", + "result": "35", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.fuel_quantity", + "unique_id": "vf1aaaaa555777123_fuel_quantity", + "result": "3", + "unit": VOLUME_LITERS, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777123_mileage", + "result": "5567", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.plug_state", + "unique_id": "vf1aaaaa555777123_plug_state", + "result": "plugged", + "class": DEVICE_CLASS_PLUG_STATE, + }, + ], + }, + "captur_fuel": { + "expected_device": { + "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, + "manufacturer": "Renault", + "model": "Captur ii", + "name": "REG-NUMBER", + "sw_version": "XJB1SU", + }, + "endpoints_available": [ + True, # cockpit + False, # hvac-status + # Ignore, # battery-status + # Ignore, # charge-mode + ], + "endpoints": {"cockpit": "cockpit_fuel.json"}, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.fuel_autonomy", + "unique_id": "vf1aaaaa555777123_fuel_autonomy", + "result": "35", + "unit": LENGTH_KILOMETERS, + }, + { + "entity_id": "sensor.fuel_quantity", + "unique_id": "vf1aaaaa555777123_fuel_quantity", + "result": "3", + "unit": VOLUME_LITERS, + }, + { + "entity_id": "sensor.mileage", + "unique_id": "vf1aaaaa555777123_mileage", + "result": "5567", + "unit": LENGTH_KILOMETERS, + }, + ], + }, +} diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py new file mode 100644 index 00000000000..c8b9c8c3e12 --- /dev/null +++ b/tests/components/renault/test_config_flow.py @@ -0,0 +1,137 @@ +"""Test the Renault config flow.""" +from unittest.mock import AsyncMock, PropertyMock, patch + +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon import schemas + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.renault.const import ( + CONF_KAMEREON_ACCOUNT_ID, + CONF_LOCALE, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + + +async def test_config_flow_single_account(hass: HomeAssistant): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Failed credentials + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_1") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + # Account list single + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_1" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert result["data"][CONF_LOCALE] == "fr_FR" + + +async def test_config_flow_no_account(hass: HomeAssistant): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Account list empty + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", + return_value=[], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "kamereon_no_account" + + +async def test_config_flow_multiple_accounts(hass: HomeAssistant): + """Test what happens if multiple Kamereon accounts are available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Multiple accounts + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", + return_value=["account_id_1", "account_id_2"], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "kamereon" + + # Account selected + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_2" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" + assert result["data"][CONF_LOCALE] == "fr_FR" diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py new file mode 100644 index 00000000000..974155c3df9 --- /dev/null +++ b/tests/components/renault/test_init.py @@ -0,0 +1,85 @@ +"""Tests for Renault setup process.""" +from unittest.mock import AsyncMock, patch + +import aiohttp +import pytest +from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon import schemas + +from homeassistant.components.renault import ( + RenaultHub, + async_setup_entry, + async_unload_entry, +) +from homeassistant.components.renault.const import DOMAIN +from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry, load_fixture + + +async def test_setup_unload_and_reload_entry(hass): + """Test entry setup and unload.""" + # Create a mock entry so we don't have to go through config flow + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + renault_account = AsyncMock() + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ): + # Set up the entry and assert that the values set during setup are where we expect + # them to be. + assert await async_setup_entry(hass, config_entry) + assert DOMAIN in hass.data and config_entry.unique_id in hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN][config_entry.unique_id], RenaultHub) + + renault_hub: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] + assert len(renault_hub.vehicles) == 1 + assert isinstance( + renault_hub.vehicles["VF1AAAAA555777999"], RenaultVehicleProxy + ) + + # Unload the entry and verify that the data has been removed + assert await async_unload_entry(hass, config_entry) + assert config_entry.unique_id not in hass.data[DOMAIN] + + +async def test_setup_entry_bad_password(hass): + """Test entry setup and unload.""" + # Create a mock entry so we don't have to go through config flow + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): + # Set up the entry and assert that the values set during setup are where we expect + # them to be. + assert not await async_setup_entry(hass, config_entry) + + +async def test_setup_entry_exception(hass): + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 + ) + + # In this case we are testing the condition where async_setup_entry raises + # ConfigEntryNotReady. + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=aiohttp.ClientConnectionError, + ), pytest.raises(ConfigEntryNotReady): + assert await async_setup_entry(hass, config_entry) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py new file mode 100644 index 00000000000..8956fa7e7e6 --- /dev/null +++ b/tests/components/renault/test_sensor.py @@ -0,0 +1,212 @@ +"""Tests for Renault sensors.""" +from unittest.mock import PropertyMock, patch + +import pytest +from renault_api.kamereon import exceptions + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from . import ( + create_vehicle_proxy, + create_vehicle_proxy_with_side_effect, + setup_renault_integration, +) +from .const import MOCK_VEHICLES + +from tests.common import mock_device_registry, mock_registry + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensors(hass, vehicle_type): + """Test for Renault sensors.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_proxy = await create_vehicle_proxy(hass, vehicle_type) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == expected_entity["result"] + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensor_empty(hass, vehicle_type): + """Test for Renault sensors with empty data from Renault.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect(hass, vehicle_type, {}) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_sensor_errors(hass, vehicle_type): + """Test for Renault sensors with temporary failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + invalid_upstream_exception = exceptions.InvalidUpstreamException( + "err.tech.500", + "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + assert len(device_registry.devices) == 1 + expected_device = mock_vehicle["expected_device"] + registry_entry = device_registry.async_get_device(expected_device["identifiers"]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device["identifiers"] + assert registry_entry.manufacturer == expected_device["manufacturer"] + assert registry_entry.name == expected_device["name"] + assert registry_entry.model == expected_device["model"] + assert registry_entry.sw_version == expected_device["sw_version"] + + expected_entities = mock_vehicle[SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +async def test_sensor_access_denied(hass): + """Test for Renault sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, "zoe_40", access_denied_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 0 + assert len(entity_registry.entities) == 0 + + +async def test_sensor_not_supported(hass): + """Test for Renault sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + vehicle_proxy = await create_vehicle_proxy_with_side_effect( + hass, "zoe_40", not_supported_exception + ) + + with patch( + "homeassistant.components.renault.RenaultHub.vehicles", + new_callable=PropertyMock, + return_value={ + vehicle_proxy.details.vin: vehicle_proxy, + }, + ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration(hass) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 0 + assert len(entity_registry.entities) == 0 diff --git a/tests/fixtures/renault/battery_status_charging.json b/tests/fixtures/renault/battery_status_charging.json new file mode 100644 index 00000000000..dbde4597e93 --- /dev/null +++ b/tests/fixtures/renault/battery_status_charging.json @@ -0,0 +1,18 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "timestamp": "2020-01-12T21:40:16Z", + "batteryLevel": 60, + "batteryTemperature": 20, + "batteryAutonomy": 141, + "batteryCapacity": 0, + "batteryAvailableEnergy": 31, + "plugStatus": 1, + "chargingStatus": 1.0, + "chargingRemainingTime": 145, + "chargingInstantaneousPower": 27 + } + } +} diff --git a/tests/fixtures/renault/battery_status_not_charging.json b/tests/fixtures/renault/battery_status_not_charging.json new file mode 100644 index 00000000000..750d0081ed9 --- /dev/null +++ b/tests/fixtures/renault/battery_status_not_charging.json @@ -0,0 +1,15 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "timestamp": "2020-11-17T09:06:48+01:00", + "batteryLevel": 50, + "batteryAutonomy": 128, + "batteryCapacity": 0, + "batteryAvailableEnergy": 0, + "plugStatus": 0, + "chargingStatus": -1.0 + } + } +} diff --git a/tests/fixtures/renault/charge_mode_always.json b/tests/fixtures/renault/charge_mode_always.json new file mode 100644 index 00000000000..6f146a2f72f --- /dev/null +++ b/tests/fixtures/renault/charge_mode_always.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "chargeMode": "always" } + } +} diff --git a/tests/fixtures/renault/charge_mode_schedule.json b/tests/fixtures/renault/charge_mode_schedule.json new file mode 100644 index 00000000000..778994746ff --- /dev/null +++ b/tests/fixtures/renault/charge_mode_schedule.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "chargeMode": "schedule_mode" } + } +} diff --git a/tests/fixtures/renault/cockpit_ev.json b/tests/fixtures/renault/cockpit_ev.json new file mode 100644 index 00000000000..c5a390f3dda --- /dev/null +++ b/tests/fixtures/renault/cockpit_ev.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "totalMileage": 49114.27 + } + } +} diff --git a/tests/fixtures/renault/cockpit_fuel.json b/tests/fixtures/renault/cockpit_fuel.json new file mode 100644 index 00000000000..575a4236c19 --- /dev/null +++ b/tests/fixtures/renault/cockpit_fuel.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777123", + "attributes": { + "fuelAutonomy": 35.0, + "fuelQuantity": 3.0, + "totalMileage": 5566.78 + } + } +} diff --git a/tests/fixtures/renault/hvac_status.json b/tests/fixtures/renault/hvac_status.json new file mode 100644 index 00000000000..f48cbae68ae --- /dev/null +++ b/tests/fixtures/renault/hvac_status.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } + } +} diff --git a/tests/fixtures/renault/vehicle_captur_fuel.json b/tests/fixtures/renault/vehicle_captur_fuel.json new file mode 100644 index 00000000000..3aa854c61ea --- /dev/null +++ b/tests/fixtures/renault/vehicle_captur_fuel.json @@ -0,0 +1,108 @@ +{ + "accountId": "account-id-1", + "country": "LU", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777123", + "status": "ACTIVE", + "linkType": "USER", + "garageBrand": "RENAULT", + "mileage": 346, + "startDate": "2020-06-12", + "createdDate": "2020-06-12T15:02:00.555432Z", + "lastModifiedDate": "2020-06-15T06:21:43.762467Z", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2020-06-15T06:20:39.107794Z", + "lastModifiedDate": "2020-06-15T06:20:39.107794Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777123", + "engineType": "H5H", + "engineRatio": "470", + "modelSCR": "CP1", + "deliveryCountry": { + "code": "BE", + "label": "BELGIQUE" + }, + "family": { + "code": "XJB", + "label": "FAMILLE B+X OVER", + "group": "007" + }, + "tcu": { + "code": "AIVCT", + "label": "AVEC BOITIER CONNECT AIVC", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "", + "label": "", + "group": "" + }, + "battery": { + "code": "SANBAT", + "label": "SANS BATTERIE", + "group": "968" + }, + "radioType": { + "code": "NA406", + "label": "A-IVIMINDL, 2BO + 2BI + 2T, MICRO-DOUBLE, FM1/DAB+FM2", + "group": "425" + }, + "registrationCountry": { + "code": "BE" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "XJB1SU", + "label": "CAPTUR II", + "group": "971" + }, + "gearbox": { + "code": "BVA7", + "label": "BOITE DE VITESSE AUTOMATIQUE 7 RAPPORTS", + "group": "427" + }, + "version": { + "code": "ITAMFHA 6TH" + }, + "energy": { + "code": "ESS", + "label": "ESSENCE", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "ADR00/DLIGM2/PGPRT2/FEUAR3/CDVIT1/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET04/DANGMO/ECOMOD/SSRCAR/AIVCT/AVGSI/TPRPE/TSGNE/2TON/ITPK7/MLEXP1/SPERTA/SSPERG/SPERTP/VOLCHA/SREACT/AVOSP1/SWALBO/DWGE01/AVC1A/1234Y/AEBS07/PRAHL/AVCAM/STANDA/XJB/HJB/EA3/MF/ESS/DG/TEMP/TR4X2/AFURGE/RVDIST/ABS/SBARTO/CA02/TOPAN/PBNCH/LAC/VSTLAR/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV02/SGAR02/BIXPE/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/SCACBA/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETIN2/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGACHA/BEL01/APL03/FSTPO/ALOUC5/CMAR3P/FIPOU2/NA406/BVA7/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRGAC/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06T/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/SSTYAD/HYB01/SSCABA/SANBAT/VEC012/XJB1SU/SSNBT/H5H", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "NONE", + "easyConnectStore": false, + "electrical": false, + "rlinkStore": false, + "deliveryDate": "2020-06-17", + "retrievedFromDhs": false, + "engineEnergyType": "OTHER", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_captur_phev.json b/tests/fixtures/renault/vehicle_captur_phev.json new file mode 100644 index 00000000000..03066c8238f --- /dev/null +++ b/tests/fixtures/renault/vehicle_captur_phev.json @@ -0,0 +1,110 @@ +{ + "accountId": "account-id-2", + "country": "IT", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777123", + "status": "ACTIVE", + "linkType": "OWNER", + "garageBrand": "RENAULT", + "startDate": "2020-10-07", + "createdDate": "2020-10-07T09:17:44.692802Z", + "lastModifiedDate": "2021-03-28T10:44:01.139649Z", + "ownershipStartDate": "2020-09-30", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2020-10-08T17:36:39.445523Z", + "lastModifiedDate": "2020-10-08T17:36:39.445523Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777123", + "registrationDate": "2020-09-30", + "firstRegistrationDate": "2020-09-30", + "engineType": "H4M", + "engineRatio": "630", + "modelSCR": "", + "deliveryCountry": { + "code": "IT", + "label": "ITALY" + }, + "family": { + "code": "XJB", + "label": "B+X OVER FAMILY", + "group": "007" + }, + "tcu": { + "code": "AIVCT", + "label": "WITH AIVC CONNECTION UNIT", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "", + "label": "", + "group": "" + }, + "battery": { + "code": "BT9AE1", + "label": "BATTERY BT9AE1", + "group": "968" + }, + "radioType": { + "code": "NA418", + "label": "FULL NAV DAB ETH - AUDI", + "group": "425" + }, + "registrationCountry": { + "code": "IT" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "XJB1SU", + "label": "CAPTUR II", + "group": "971" + }, + "gearbox": { + "code": "BVH4", + "label": "HYBRID 4 SPEED GEARBOX", + "group": "427" + }, + "version": { + "code": "ITAMMHH 6UP" + }, + "energy": { + "code": "ESS", + "label": "PETROL", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "STANDA/XJB/HJB/EA3/MM/ESS/DG/TEMP/TR4X2/AFURGE/RV/ABS/SBARTO/CA02/TN/PBNCH/LAC/VT/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV01/SGAR02/BIYPC/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/CACBL3/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGSCHA/ITA01/APL03/FSTPO/ALOUC5/PART01/CMAR3P/FIPOU2/NA418/BVH4/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRFLY/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06U/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/5DHS/HYB06/010KWH/BT9AE1/VEC237/XJB1SU/NBT018/H4M/NOADR/DLIGM2/PGPRT2/FEUAR3/SCDVIT/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET05/SDANGM/ECOMOD/SSRCAR/AIVCT/AVGSI/TPQNW/TSGNE/2TON/ITPK4/MLEXP1/SPERTA/SSPERG/SPERTP/VOLNCH/SREACT/AVTSR1/SWALBO/DWGE01/AVC1A/VSPTA/1234Y/AEBS07/PRAHL/RRCAM", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "NONE", + "easyConnectStore": false, + "electrical": false, + "rlinkStore": false, + "deliveryDate": "2020-09-30", + "retrievedFromDhs": false, + "engineEnergyType": "PHEV", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_zoe_40.json b/tests/fixtures/renault/vehicle_zoe_40.json new file mode 100644 index 00000000000..ab80d586652 --- /dev/null +++ b/tests/fixtures/renault/vehicle_zoe_40.json @@ -0,0 +1,189 @@ +{ + "accountId": "account-id-1", + "country": "FR", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777999", + "status": "ACTIVE", + "linkType": "OWNER", + "garageBrand": "RENAULT", + "annualMileage": 16000, + "mileage": 26464, + "startDate": "2017-08-07", + "createdDate": "2019-05-23T21:38:16.409008Z", + "lastModifiedDate": "2020-11-17T08:41:40.497400Z", + "ownershipStartDate": "2017-08-01", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2019-06-17T09:49:06.880627Z", + "lastModifiedDate": "2019-06-17T09:49:06.880627Z" + }, + "vehicleDetails": { + "vin": "VF1AAAAA555777999", + "registrationDate": "2017-08-01", + "firstRegistrationDate": "2017-08-01", + "engineType": "5AQ", + "engineRatio": "601", + "modelSCR": "ZOE", + "deliveryCountry": { + "code": "FR", + "label": "FRANCE" + }, + "family": { + "code": "X10", + "label": "FAMILLE X10", + "group": "007" + }, + "tcu": { + "code": "TCU0G2", + "label": "TCU VER 0 GEN 2", + "group": "E70" + }, + "navigationAssistanceLevel": { + "code": "NAV3G5", + "label": "LEVEL 3 TYPE 5 NAVIGATION", + "group": "408" + }, + "battery": { + "code": "BT4AR1", + "label": "BATTERIE BT4AR1", + "group": "968" + }, + "radioType": { + "code": "RAD37A", + "label": "RADIO 37A", + "group": "425" + }, + "registrationCountry": { + "code": "FR" + }, + "brand": { + "label": "RENAULT" + }, + "model": { + "code": "X101VE", + "label": "ZOE", + "group": "971" + }, + "gearbox": { + "code": "BVEL", + "label": "BOITE A VARIATEUR ELECTRIQUE", + "group": "427" + }, + "version": { + "code": "INT MB 10R" + }, + "energy": { + "code": "ELEC", + "label": "ELECTRIQUE", + "group": "019" + }, + "registrationNumber": "REG-NUMBER", + "vcd": "SYTINC/SKTPOU/SAND41/FDIU1/SSESM/MAPSUP/SSCALL/SAND88/SAND90/SQKDRO/SDIFPA/FACBA2/PRLEX1/SSRCAR/CABDO2/TCU0G2/SWALBO/EVTEC1/STANDA/X10/B10/EA2/MB/ELEC/DG/TEMP/TR4X2/RV/ABS/CAREG/LAC/VT003/CPE/RET03/SPROJA/RALU16/CEAVRH/AIRBA1/SERIE/DRA/DRAP08/HARM02/ATAR/TERQG/SFBANA/KM/DPRPN/AVREPL/SSDECA/ASRESP/RDAR02/ALEVA/CACBL2/SOP02C/CTHAB2/TRNOR/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/APL03/ALOUCC/CMAR3P/NAV3G5/RAD37A/BVEL/AUTAUG/RNORM/ISOFIX/EQPEUR/HRGM01/SDPCLV/TLFRAN/SPRODI/SAN613/SSAPEX/GENEV1/ELC1/SANCML/PE2012/PHAS1/SAN913/045KWH/BT4AR1/VEC153/X101VE/NBT017/5AQ", + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + }, + { + "assetType": "PDF", + "assetRole": "GUIDE", + "title": "PDF Guide", + "description": "", + "renditions": [ + { + "url": "https://cdn.group.renault.com/ren/gb/myr/assets/x101ve/manual.pdf.asset.pdf/1558704861676.pdf" + } + ] + }, + { + "assetType": "URL", + "assetRole": "GUIDE", + "title": "e-guide", + "description": "", + "renditions": [ + { + "url": "http://gb.e-guide.renault.com/eng/Zoe" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "10 Fundamentals about getting the best out of your electric vehicle", + "description": "", + "renditions": [ + { + "url": "39r6QEKcOM4" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Automatic Climate Control", + "description": "", + "renditions": [ + { + "url": "Va2FnZFo_GE" + } + ] + }, + { + "assetType": "URL", + "assetRole": "CAR", + "title": "More videos", + "description": "", + "renditions": [ + { + "url": "https://www.youtube.com/watch?v=wfpCMkK1rKI" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Charging the battery", + "description": "", + "renditions": [ + { + "url": "RaEad8DjUJs" + } + ] + }, + { + "assetType": "VIDEO", + "assetRole": "CAR", + "title": "Charging the battery at a station with a flap", + "description": "", + "renditions": [ + { + "url": "zJfd7fJWtr0" + } + ] + } + ], + "yearsOfMaintenance": 12, + "connectivityTechnology": "RLINK1", + "easyConnectStore": false, + "electrical": true, + "rlinkStore": false, + "deliveryDate": "2017-08-11", + "retrievedFromDhs": false, + "engineEnergyType": "ELEC", + "radioCode": "1234" + } + } + ] +} diff --git a/tests/fixtures/renault/vehicle_zoe_50.json b/tests/fixtures/renault/vehicle_zoe_50.json new file mode 100644 index 00000000000..560b2a2246a --- /dev/null +++ b/tests/fixtures/renault/vehicle_zoe_50.json @@ -0,0 +1,161 @@ +{ + "country": "GB", + "vehicleLinks": [ + { + "preferredDealer": { + "brand": "RENAULT", + "createdDate": "2019-05-23T20:42:01.086661Z", + "lastModifiedDate": "2019-05-23T20:42:01.086662Z", + "dealerId": "dealer-id-1" + }, + "garageBrand": "RENAULT", + "vehicleDetails": { + "assets": [ + { + "assetType": "PICTURE", + "renditions": [ + { + "resolutionType": "ONE_MYRENAULT_LARGE", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE" + }, + { + "resolutionType": "ONE_MYRENAULT_SMALL", + "url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2" + } + ] + }, + { + "title": "PDF Guide", + "description": "", + "assetType": "PDF", + "assetRole": "GUIDE", + "renditions": [ + { + "url": "https://cdn.group.renault.com/ren/gb/myr/assets/x102ve/manual.pdf.asset.pdf/1558696740707.pdf" + } + ] + }, + { + "title": "e-guide", + "description": "", + "assetType": "URL", + "assetRole": "GUIDE", + "renditions": [ + { + "url": "https://gb.e-guide.renault.com/eng/Zoe-ph2" + } + ] + }, + { + "title": "All-New ZOE: Welcome to your new car", + "description": "", + "assetType": "VIDEO", + "assetRole": "CAR", + "renditions": [ + { + "url": "1OGwwmWHB6o" + } + ] + }, + { + "title": "Renault ZOE: All you need to know", + "description": "", + "assetType": "VIDEO", + "assetRole": "CAR", + "renditions": [ + { + "url": "_BVH-Rd6e5I" + } + ] + } + ], + "engineType": "5AQ", + "registrationCountry": { + "code": "FR" + }, + "radioType": { + "group": "425", + "code": "NA418", + "label": " FULL NAV DAB ETH - AUDI" + }, + "tcu": { + "group": "E70", + "code": "AIVCT", + "label": "AVEC BOITIER CONNECT AIVC" + }, + "brand": { + "label": "RENAULT" + }, + "deliveryDate": "2020-01-22", + "engineEnergyType": "ELEC", + "registrationDate": "2020-01-13", + "gearbox": { + "group": "427", + "code": "BVEL", + "label": "BOITE A VARIATEUR ELECTRIQUE" + }, + "model": { + "group": "971", + "code": "X102VE", + "label": "ZOE" + }, + "electrical": true, + "energy": { + "group": "019", + "code": "ELEC", + "label": "ELECTRIQUE" + }, + "navigationAssistanceLevel": { + "group": "408", + "code": "SAN408", + "label": "CRITERE DE CONTEXTE" + }, + "yearsOfMaintenance": 12, + "rlinkStore": false, + "radioCode": "1234", + "registrationNumber": "REG-NUMBER", + "modelSCR": "ZOE", + "easyConnectStore": false, + "engineRatio": "605", + "battery": { + "group": "968", + "code": "BT4AR1", + "label": "BATTERIE BT4AR1" + }, + "vin": "VF1AAAAA555777999", + "retrievedFromDhs": false, + "vcd": "ASCOD0/DLIGM2/SSTINC/KITPOU/SKTPGR/SSCCPC/SDPSEC/FDIU2/SSMAP/SSCALL/FACBA1/DANGMO/SSRCAR/SSCABD/AIVCT/AVGSI/ITPK4/VOLNCH/REACTI/AVOSP1/SWALBO/SSDWGE/1234Y/SSAEBS/PRAHL/RRCAM/STANDA/X10/B10/EA3/MD/ELEC/DG/TEMP/TR4X2/AFURGE/RV/ABS/CAREG/LAC/VSTLAR/CPETIR/RET03/PROJAB/RALU16/CEAVRH/ADAC/AIRBA2/SERIE/DRA/DRAP13/HARM02/3ATRPH/SGAV01/BARRAB/TELNJ/SFBANA/KM/DPRPN/AVREPL/SSDECA/ABLAV/ASRESP/ALEVA/SCACBA/SOP02C/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/RETC/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/FRA01/APL03/FSTPO/ALOUC5/CMAR3P/SAN408/NA418/BVEL/AUTAUG/SPREST/RDIF01/ISOFIX/EQPEUR/HRGM01/SDPCLV/CHASTD/TL01A/SPRODI/SAN613/AIRBDE/PSMREC/ELC1/SSPTLP/SANCML/SEXTIN/PE2019/PHAS2/SAN913/THABT2/SSTYAD/SSHYB/052KWH/BT4AR1/VEC018/X102VE/NBT022/5AQ", + "firstRegistrationDate": "2020-01-13", + "deliveryCountry": { + "code": "FR", + "label": "FRANCE" + }, + "connectivityTechnology": "RLINK1", + "family": { + "group": "007", + "code": "X10", + "label": "FAMILLE X10" + }, + "version": { + "code": "INT A MD 1L" + } + }, + "status": "ACTIVE", + "createdDate": "2020-08-21T16:48:00.243967Z", + "cancellationReason": {}, + "linkType": "OWNER", + "connectedDriver": { + "role": "MAIN_DRIVER", + "lastModifiedDate": "2020-08-22T09:41:53.477398Z", + "createdDate": "2020-08-22T09:41:53.477398Z" + }, + "vin": "VF1AAAAA555777999", + "lastModifiedDate": "2020-11-29T22:01:21.162572Z", + "brand": "RENAULT", + "startDate": "2020-08-21", + "ownershipStartDate": "2020-01-13", + "ownershipEndDate": "2020-08-21" + } + ], + "accountId": "account-id-1" +} From 3265c7b8d89569eab411c4a2f9606c64a2e306da Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 28 Jul 2021 17:15:27 -0400 Subject: [PATCH 003/199] Add zwave_js.reset_meter service (#53390) * Add zwave_js.meter_reset service * fix log statement * Add endpoint attribute to service call and rename service * Make service an entity service * remove endpoint from service description --- homeassistant/components/zwave_js/const.py | 22 +++---- homeassistant/components/zwave_js/sensor.py | 38 +++++++++++- .../components/zwave_js/services.yaml | 23 ++++++++ tests/components/zwave_js/test_sensor.py | 59 +++++++++++++++++++ 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ae5607745f6..7848af146b5 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -49,24 +49,24 @@ ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" # service constants -ATTR_NODES = "nodes" - +SERVICE_SET_VALUE = "set_value" +SERVICE_RESET_METER = "reset_meter" +SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" +SERVICE_PING = "ping" +SERVICE_REFRESH_VALUE = "refresh_value" SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters" +ATTR_NODES = "nodes" +# config parameter ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" ATTR_CONFIG_VALUE = "value" - -SERVICE_REFRESH_VALUE = "refresh_value" - +# refresh value ATTR_REFRESH_ALL_VALUES = "refresh_all_values" - -SERVICE_SET_VALUE = "set_value" -SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" - +# multicast ATTR_BROADCAST = "broadcast" - -SERVICE_PING = "ping" +# meter reset +ATTR_METER_TYPE = "meter_type" ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index f20a12a519a..7c41ad035be 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import cast +import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, ConfigurationValueType from zwave_js_server.model.node import Node as ZwaveNode @@ -26,10 +27,11 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .helpers import get_device_id @@ -89,6 +91,16 @@ async def async_setup_entry( ) ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RESET_METER, + { + vol.Optional(ATTR_METER_TYPE): vol.Coerce(int), + vol.Optional(ATTR_VALUE): vol.Coerce(int), + }, + "async_reset_meter", + ) + class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" @@ -218,6 +230,30 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) + async def async_reset_meter( + self, meter_type: int | None = None, value: int | None = None + ) -> None: + """Reset meter(s) on device.""" + node = self.info.node + primary_value = self.info.primary_value + if primary_value.command_class != CommandClass.METER: + raise TypeError("Reset only available for Meter sensors") + options = {} + if meter_type is not None: + options["type"] = meter_type + if value is not None: + options["targetValue"] = value + args = [options] if options else [] + await node.endpoints[primary_value.endpoint].async_invoke_cc_api( + CommandClass.METER, "reset", *args, wait_for_result=False + ) + LOGGER.debug( + "Meters on node %s endpoint %s reset with the following options: %s", + node, + primary_value.endpoint, + options, + ) + class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor with multiple states.""" diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index f737f159806..b41a893c7e4 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -229,3 +229,26 @@ ping: target: entity: integration: zwave_js + +reset_meter: + name: Reset meter(s) on a node + description: Resets the meter(s) on a node. + target: + entity: + domain: sensor + integration: zwave_js + fields: + meter_type: + name: Meter Type + description: The type of meter to reset. Not all meters support the ability to pick a meter type to reset. + example: 1 + required: false + selector: + text: + value: + name: Target Value + description: The value that meter(s) should be reset to. Not all meters support the ability to be reset to a specific value. + example: 5 + required: false + selector: + text: diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index fc6d274235d..e368ec1b026 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,7 +1,14 @@ """Test the Z-Wave JS sensor platform.""" from zwave_js_server.event import Event +from homeassistant.components.zwave_js.const import ( + ATTR_METER_TYPE, + ATTR_VALUE, + DOMAIN, + SERVICE_RESET_METER, +) from homeassistant.const import ( + ATTR_ENTITY_ID, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, @@ -131,3 +138,55 @@ async def test_node_status_sensor(hass, lock_id_lock_as_id150, integration): ) node.receive_event(event) assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" + + +async def test_reset_meter( + hass, + client, + aeon_smart_switch_6, + integration, +): + """Test reset_meter service.""" + SENSOR = "sensor.smart_switch_6_electric_consumed_v" + client.async_send_command.return_value = {} + client.async_send_command_no_wait.return_value = {} + + # Test successful meter reset call + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: SENSOR, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "endpoint.invoke_cc_api" + assert args["nodeId"] == aeon_smart_switch_6.node_id + assert args["endpoint"] == 0 + assert args["args"] == [] + + client.async_send_command_no_wait.reset_mock() + + # Test successful meter reset call with options + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: SENSOR, + ATTR_METER_TYPE: 1, + ATTR_VALUE: 2, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "endpoint.invoke_cc_api" + assert args["nodeId"] == aeon_smart_switch_6.node_id + assert args["endpoint"] == 0 + assert args["args"] == [{"type": 1, "targetValue": 2}] + + client.async_send_command_no_wait.reset_mock() From f13d7f189ade8f241f761ff21bf0e010e732b8ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 18:09:49 -0500 Subject: [PATCH 004/199] Fix invalid homekit state when arming (#53646) - Maybe fixes #48538 --- .../homekit/type_security_systems.py | 141 +++++++++--------- .../homekit/test_type_security_systems.py | 63 +++++++- 2 files changed, 127 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index acbf636c1c3..6fe1a4e9e29 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -2,13 +2,13 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM -from pyhap.loader import get_loader from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, ) from homeassistant.const import ( @@ -22,6 +22,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) @@ -36,28 +38,43 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = { - STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, - STATE_ALARM_ARMED_NIGHT: 2, - STATE_ALARM_DISARMED: 3, - STATE_ALARM_TRIGGERED: 4, +HK_ALARM_STAY_ARMED = 0 +HK_ALARM_AWAY_ARMED = 1 +HK_ALARM_NIGHT_ARMED = 2 +HK_ALARM_DISARMED = 3 +HK_ALARM_TRIGGERED = 4 + +HASS_TO_HOMEKIT_CURRENT = { + STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED, + STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED, + STATE_ALARM_ARMING: HK_ALARM_DISARMED, + STATE_ALARM_DISARMED: HK_ALARM_DISARMED, + STATE_ALARM_TRIGGERED: HK_ALARM_TRIGGERED, +} + +HASS_TO_HOMEKIT_TARGET = { + STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED, + STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED, + STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED, + STATE_ALARM_ARMING: HK_ALARM_AWAY_ARMED, + STATE_ALARM_DISARMED: HK_ALARM_DISARMED, } HASS_TO_HOMEKIT_SERVICES = { - SERVICE_ALARM_ARM_HOME: 0, - SERVICE_ALARM_ARM_AWAY: 1, - SERVICE_ALARM_ARM_NIGHT: 2, - SERVICE_ALARM_DISARM: 3, + SERVICE_ALARM_ARM_HOME: HK_ALARM_STAY_ARMED, + SERVICE_ALARM_ARM_AWAY: HK_ALARM_AWAY_ARMED, + SERVICE_ALARM_ARM_NIGHT: HK_ALARM_NIGHT_ARMED, + SERVICE_ALARM_DISARM: HK_ALARM_DISARMED, } -HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} - -STATE_TO_SERVICE = { - STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, - STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, - STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, - STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM, +HK_TO_SERVICE = { + HK_ALARM_AWAY_ARMED: SERVICE_ALARM_ARM_AWAY, + HK_ALARM_STAY_ARMED: SERVICE_ALARM_ARM_HOME, + HK_ALARM_NIGHT_ARMED: SERVICE_ALARM_ARM_NIGHT, + HK_ALARM_DISARMED: SERVICE_ALARM_DISARM, } @@ -75,65 +92,51 @@ class SecuritySystem(HomeAccessory): ATTR_SUPPORTED_FEATURES, ( SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_VACATION | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_TRIGGER ), ) - loader = get_loader() - default_current_states = loader.get_char( - "SecuritySystemCurrentState" - ).properties.get("ValidValues") - default_target_services = loader.get_char( - "SecuritySystemTargetState" - ).properties.get("ValidValues") + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) + current_char = serv_alarm.get_characteristic(CHAR_CURRENT_SECURITY_STATE) + target_char = serv_alarm.get_characteristic(CHAR_TARGET_SECURITY_STATE) + default_current_states = current_char.properties.get("ValidValues") + default_target_services = target_char.properties.get("ValidValues") - current_supported_states = [ - HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], - HASS_TO_HOMEKIT[STATE_ALARM_TRIGGERED], - ] - target_supported_services = [HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM]] + current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED] + target_supported_services = [HK_ALARM_DISARMED] if supported_states & SUPPORT_ALARM_ARM_HOME: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_HOME]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_HOME] - ) + current_supported_states.append(HK_ALARM_STAY_ARMED) + target_supported_services.append(HK_ALARM_STAY_ARMED) - if supported_states & SUPPORT_ALARM_ARM_AWAY: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_AWAY]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_AWAY] - ) + if supported_states & (SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_VACATION): + current_supported_states.append(HK_ALARM_AWAY_ARMED) + target_supported_services.append(HK_ALARM_AWAY_ARMED) if supported_states & SUPPORT_ALARM_ARM_NIGHT: - current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_NIGHT]) - target_supported_services.append( - HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_NIGHT] - ) + current_supported_states.append(HK_ALARM_NIGHT_ARMED) + target_supported_services.append(HK_ALARM_NIGHT_ARMED) - new_current_states = { - key: val - for key, val in default_current_states.items() - if val in current_supported_states - } - new_target_services = { - key: val - for key, val in default_target_services.items() - if val in target_supported_services - } - - serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( CHAR_CURRENT_SECURITY_STATE, - value=HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], - valid_values=new_current_states, + value=HASS_TO_HOMEKIT_CURRENT[STATE_ALARM_DISARMED], + valid_values={ + key: val + for key, val in default_current_states.items() + if val in current_supported_states + }, ) self.char_target_state = serv_alarm.configure_char( CHAR_TARGET_SECURITY_STATE, value=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM], - valid_values=new_target_services, + valid_values={ + key: val + for key, val in default_target_services.items() + if val in target_supported_services + }, setter_callback=self.set_security_state, ) @@ -144,9 +147,7 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set security state to %d", self.entity_id, value) - hass_value = HOMEKIT_TO_HASS[value] - service = STATE_TO_SERVICE[hass_value] - + service = HK_TO_SERVICE[value] params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code @@ -156,20 +157,16 @@ class SecuritySystem(HomeAccessory): def async_update_state(self, new_state): """Update security state after state changed.""" hass_state = new_state.state - if hass_state in HASS_TO_HOMEKIT: - current_security_state = HASS_TO_HOMEKIT[hass_state] - if self.char_current_state.value != current_security_state: - self.char_current_state.set_value(current_security_state) + if (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None: + if self.char_current_state.value != current_state: + self.char_current_state.set_value(current_state) _LOGGER.debug( "%s: Updated current state to %s (%d)", self.entity_id, hass_state, - current_security_state, + current_state, ) - # SecuritySystemTargetState does not support triggered - if ( - hass_state != STATE_ALARM_TRIGGERED - and self.char_target_state.value != current_security_state - ): - self.char_target_state.set_value(current_security_state) + if (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None: + if self.char_target_state.value != target_state: + self.char_target_state.set_value(target_state) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 19b8b5720e2..d1ce830a0e2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -17,6 +17,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, @@ -79,7 +81,7 @@ async def test_switch_set_state(hass, hk_driver, events): call_arm_night = async_mock_service(hass, DOMAIN, "alarm_arm_night") call_disarm = async_mock_service(hass, DOMAIN, "alarm_disarm") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_arm_home assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id @@ -88,7 +90,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 1) + acc.char_target_state.client_update_value(1) await hass.async_block_till_done() assert call_arm_away assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id @@ -97,7 +99,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 2) + acc.char_target_state.client_update_value(2) await hass.async_block_till_done() assert call_arm_night assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id @@ -106,7 +108,7 @@ async def test_switch_set_state(hass, hk_driver, events): assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 3) + acc.char_target_state.client_update_value(3) await hass.async_block_till_done() assert call_disarm assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id @@ -128,7 +130,7 @@ async def test_no_alarm_code(hass, hk_driver, config, events): # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, "alarm_arm_home") - await hass.async_add_executor_job(acc.char_target_state.client_update_value, 0) + acc.char_target_state.client_update_value(0) await hass.async_block_till_done() assert call_arm_home assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id @@ -138,6 +140,57 @@ async def test_no_alarm_code(hass, hk_driver, config, events): assert events[-1].data[ATTR_VALUE] is None +async def test_arming(hass, hk_driver, events): + """Test to make sure arming sets the right state.""" + entity_id = "alarm_control_panel.test" + + hass.states.async_set(entity_id, None) + + acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, {}) + await acc.run() + await hass.async_block_till_done() + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert acc.char_target_state.value == 0 + assert acc.char_current_state.value == 0 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_VACATION) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert acc.char_target_state.value == 2 + assert acc.char_current_state.value == 2 + + hass.states.async_set(entity_id, STATE_ALARM_ARMING) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 3 + + hass.states.async_set(entity_id, STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 3 + + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 + + hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 4 + + async def test_supported_states(hass, hk_driver, events): """Test different supported states.""" code = "1234" From fce7417ed122eaa918e81f8311cba6e9825949ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 17:31:10 -0700 Subject: [PATCH 005/199] Add last reset to enphase sensors (#53653) --- .../components/enphase_envoy/__init__.py | 8 +- .../components/enphase_envoy/const.py | 87 +++++++++++++++---- .../components/enphase_envoy/sensor.py | 43 ++++----- 3 files changed, 88 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index dfd6b782408..69c488169a6 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -47,9 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except httpx.HTTPError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - for condition in SENSORS: - if condition != "inverters": - data[condition] = await getattr(envoy_reader, condition)() + for description in SENSORS: + if description.key != "inverters": + data[description.key] = await getattr( + envoy_reader, description.key + )() else: data[ "inverters_production" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 7a1de25e242..9f87a821787 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,8 +1,12 @@ """The enphase_envoy component.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) +from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.util import dt DOMAIN = "enphase_envoy" @@ -12,22 +16,67 @@ PLATFORMS = ["sensor"] COORDINATOR = "coordinator" NAME = "name" -SENSORS = { - "production": ("Current Energy Production", POWER_WATT, STATE_CLASS_MEASUREMENT), - "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR, None), - "seven_days_production": ( - "Last Seven Days Energy Production", - ENERGY_WATT_HOUR, - None, +SENSORS = ( + SensorEntityDescription( + key="production", + name="Current Power Production", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), - "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR, None), - "consumption": ("Current Energy Consumption", POWER_WATT, STATE_CLASS_MEASUREMENT), - "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR, None), - "seven_days_consumption": ( - "Last Seven Days Energy Consumption", - ENERGY_WATT_HOUR, - None, + SensorEntityDescription( + key="daily_production", + name="Today's Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, ), - "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR, None), - "inverters": ("Inverter", POWER_WATT, STATE_CLASS_MEASUREMENT), -} + SensorEntityDescription( + key="seven_days_production", + name="Last Seven Days Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="lifetime_production", + name="Lifetime Energy Production", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + ), + SensorEntityDescription( + key="consumption", + name="Current Power Consumption", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="daily_consumption", + name="Today's Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="seven_days_consumption", + name="Last Seven Days Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + ), + SensorEntityDescription( + key="lifetime_consumption", + name="Lifetime Energy Consumption", + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + ), + SensorEntityDescription( + key="inverters", + name="Inverter", + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), +) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 3fab9e320dc..29d273401f4 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -56,42 +56,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = data[NAME] entities = [] - for condition, sensor in SENSORS.items(): + for sensor_description in SENSORS: if ( - condition == "inverters" + sensor_description.key == "inverters" and coordinator.data.get("inverters_production") is not None ): for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name} {sensor[0]} {inverter}" + entity_name = f"{name} {sensor_description.name} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( Envoy( - condition, + sensor_description, entity_name, name, config_entry.unique_id, serial_number, - sensor[1], - sensor[2], coordinator, ) ) - elif condition != "inverters": - data = coordinator.data.get(condition) + elif sensor_description.key != "inverters": + data = coordinator.data.get(sensor_description.key) if isinstance(data, str) and "not available" in data: continue - entity_name = f"{name} {sensor[0]}" + entity_name = f"{name} {sensor_description.name}" entities.append( Envoy( - condition, + sensor_description, entity_name, name, config_entry.unique_id, None, - sensor[1], - sensor[2], coordinator, ) ) @@ -104,23 +100,19 @@ class Envoy(CoordinatorEntity, SensorEntity): def __init__( self, - sensor_type, + description, name, device_name, device_serial_number, serial_number, - unit, - state_class, coordinator, ): """Initialize Envoy entity.""" - self._type = sensor_type + self.entity_description = description self._name = name self._serial_number = serial_number self._device_name = device_name self._device_serial_number = device_serial_number - self._unit_of_measurement = unit - self._attr_state_class = state_class super().__init__(coordinator) @@ -135,16 +127,16 @@ class Envoy(CoordinatorEntity, SensorEntity): if self._serial_number: return self._serial_number if self._device_serial_number: - return f"{self._device_serial_number}_{self._type}" + return f"{self._device_serial_number}_{self.entity_description.key}" @property def state(self): """Return the state of the sensor.""" - if self._type != "inverters": - value = self.coordinator.data.get(self._type) + if self.entity_description.key != "inverters": + value = self.coordinator.data.get(self.entity_description.key) elif ( - self._type == "inverters" + self.entity_description.key == "inverters" and self.coordinator.data.get("inverters_production") is not None ): value = self.coordinator.data.get("inverters_production").get( @@ -155,11 +147,6 @@ class Envoy(CoordinatorEntity, SensorEntity): return value - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - @property def icon(self): """Icon to use in the frontend, if any.""" @@ -169,7 +156,7 @@ class Envoy(CoordinatorEntity, SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" if ( - self._type == "inverters" + self.entity_description.key == "inverters" and self.coordinator.data.get("inverters_production") is not None ): value = self.coordinator.data.get("inverters_production").get( From c9d355a8a4abe5387fee300a3d05e8c1953921bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 15:48:27 -0700 Subject: [PATCH 006/199] Add last reset to Shelly (#53654) --- homeassistant/components/shelly/entity.py | 2 ++ homeassistant/components/shelly/sensor.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 743dd07414e..c8b23f71bd7 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass +from datetime import datetime import logging from typing import Any, Callable, Final, cast @@ -179,6 +180,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None + last_reset: datetime | None = None @dataclass diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 96ff6e55f8d..7c2ffdbc470 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,7 @@ """Sensor for Shelly.""" from __future__ import annotations +from datetime import datetime from typing import Final, cast from homeassistant.components import sensor @@ -20,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt from .const import SHAIR_MAX_WORK_HOURS from .entity import ( @@ -119,6 +121,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", @@ -126,6 +129,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), ), ("light", "energy"): BlockAttributeDescription( name="Energy", @@ -257,6 +261,11 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """State class of sensor.""" return self.description.state_class + @property + def last_reset(self) -> datetime | None: + """State class of sensor.""" + return self.description.last_reset + @property def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" From 5483300668ca14138f73cf77af9e28c6d92b0e20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 19:39:45 -0500 Subject: [PATCH 007/199] Bump aiolip to 1.1.6 to fix timeout with ident (#53660) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index e0acf31e99c..b6f0785ffe7 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.4"], + "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.6"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index bfa61df2f9e..6f9381d19ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aiolifx==0.6.9 aiolifx_effects==0.2.2 # homeassistant.components.lutron_caseta -aiolip==1.1.4 +aiolip==1.1.6 # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d2e39327df..b1f1ba01960 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiohue==2.5.1 aiokafka==0.6.0 # homeassistant.components.lutron_caseta -aiolip==1.1.4 +aiolip==1.1.6 # homeassistant.components.lyric aiolyric==1.0.7 From 384ddbafab6af0bdb14915c362e2f748c527113f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jul 2021 23:58:28 -0500 Subject: [PATCH 008/199] Add device class energy and last reset to sense (#53667) --- homeassistant/components/sense/sensor.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index dd522d012a5..5a352969c3b 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,10 @@ """Support for monitoring a Sense energy sensor.""" +import datetime + from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -9,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util from .const import ( ACTIVE_NAME, @@ -218,6 +222,8 @@ class SenseVoltageSensor(SensorEntity): class SenseTrendsSensor(SensorEntity): """Implementation of a Sense energy sensor.""" + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_state_class = STATE_CLASS_MEASUREMENT _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON @@ -252,6 +258,13 @@ class SenseTrendsSensor(SensorEntity): """Return if entity is available.""" return self._had_any_update and self._coordinator.last_update_success + @property + def last_reset(self) -> datetime.datetime: + """Return the time when the sensor was last reset, if any.""" + if self._sensor_type == "DAY": + return dt_util.start_of_local_day() + return None + @callback def _async_update(self): """Track if we had an update so we do not report zero data.""" From 75dc55418bdeb3ef976600b26e5601eb356040f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 22:06:24 -0700 Subject: [PATCH 009/199] Bumped version to 2021.8.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 69a06c04f02..15d0a3485cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2ffc779f3d620b78736a3ba4265c5dc6ae667dd8 Mon Sep 17 00:00:00 2001 From: Stephen Beechen Date: Wed, 28 Jul 2021 23:12:59 -0600 Subject: [PATCH 010/199] Allow uploading large snapshots (#53528) Co-authored-by: Pascal Vizeli --- homeassistant/components/hassio/http.py | 54 +++++++------------------ tests/components/hassio/test_http.py | 49 ++++++++++++++++------ 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 302cc00bb9f..73e5549be9a 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,16 +1,15 @@ """HTTP Support for Hass.io.""" from __future__ import annotations -import asyncio import logging import os import re import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE +from aiohttp.client import ClientError, ClientTimeout +from aiohttp.hdrs import CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway -import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded @@ -20,8 +19,6 @@ from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) -MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 - NO_TIMEOUT = re.compile( r"^(?:" r"|homeassistant/update" @@ -75,48 +72,28 @@ class HassIOView(HomeAssistantView): async def _command_proxy( self, path: str, request: web.Request - ) -> web.Response | web.StreamResponse: + ) -> web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ - read_timeout = _get_timeout(path) - client_timeout = 10 - data = None headers = _init_header(request) if path in ("snapshots/new/upload", "backups/new/upload"): # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access - - # Backups are big, so we need to adjust the allowed size - request._client_max_size = ( # pylint: disable=protected-access - MAX_UPLOAD_SIZE - ) - client_timeout = 300 - try: - with async_timeout.timeout(client_timeout): - data = await request.read() - - method = getattr(self._websession, request.method.lower()) - client = await method( - f"http://{self._host}/{path}", - data=data, + # Stream the request to the supervisor + client = await self._websession.request( + method=request.method, + url=f"http://{self._host}/{path}", headers=headers, - timeout=read_timeout, + data=request.content, + timeout=_get_timeout(path), ) - # Simple request - if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: - # Return Response - body = await client.read() - return web.Response( - content_type=client.content_type, status=client.status, body=body - ) - - # Stream response + # Stream the supervisor response back response = web.StreamResponse(status=client.status, headers=client.headers) response.content_type = client.content_type @@ -126,12 +103,9 @@ class HassIOView(HomeAssistantView): return response - except aiohttp.ClientError as err: + except ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) - except asyncio.TimeoutError: - _LOGGER.error("Client timeout error on API request %s", path) - raise HTTPBadGateway() @@ -151,11 +125,11 @@ def _init_header(request: web.Request) -> dict[str, str]: return headers -def _get_timeout(path: str) -> int: +def _get_timeout(path: str) -> ClientTimeout: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): - return 0 - return 300 + return ClientTimeout(connect=10) + return ClientTimeout(connect=10, total=300) def _need_auth(hass, path: str) -> bool: diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index fc4bb3e6a0d..881d3cc26ed 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,11 +1,13 @@ """The tests for the hassio component.""" -import asyncio -from unittest.mock import patch - +from aiohttp.client import ClientError +from aiohttp.streams import StreamReader +from aiohttp.test_utils import TestClient import pytest from homeassistant.components.hassio.http import _need_auth +from tests.test_util.aiohttp import AiohttpClientMocker + async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" @@ -106,16 +108,6 @@ async def test_forward_log_request(hassio_client, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 -async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): - """Test we get a bad gateway error if we can't find supervisor.""" - with patch( - "homeassistant.components.hassio.http.async_timeout.timeout", - side_effect=asyncio.TimeoutError, - ): - resp = await hassio_client.get("/api/hassio/addons/test/info") - assert resp.status == 502 - - async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mock): """Test that we forward user info correctly.""" aioclient_mock.get("http://127.0.0.1/hello") @@ -171,6 +163,37 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): assert resp.headers["Content-Disposition"] == content_disposition +async def test_supervisor_client_error( + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker +): + """Test any client error from the supervisor returns a 502.""" + # Create a request that throws a ClientError + async def raise_client_error(*args): + raise ClientError() + + aioclient_mock.get( + "http://127.0.0.1/test/raise/error", + side_effect=raise_client_error, + ) + + # Verify it returns bad gateway + resp = await hassio_client.get("/api/hassio/test/raise/error") + assert resp.status == 502 + assert len(aioclient_mock.mock_calls) == 1 + + +async def test_streamed_requests( + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker +): + """Test requests get proxied to the supervisor as a stream.""" + aioclient_mock.get("http://127.0.0.1/test/stream") + await hassio_client.get("/api/hassio/test/stream", data="Test data") + assert len(aioclient_mock.mock_calls) == 1 + + # Verify the request body is passed as a StreamReader + assert isinstance(aioclient_mock.mock_calls[0][2], StreamReader) + + def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") From b5f0c2cef4b0533e89109e6e00535161f53d6e97 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 Jul 2021 20:02:47 +0200 Subject: [PATCH 011/199] Move TP-Link power and energy switch attributes to sensors (#53596) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/__init__.py | 127 ++++++++++-- homeassistant/components/tplink/common.py | 33 +-- homeassistant/components/tplink/const.py | 76 +++++++ homeassistant/components/tplink/sensor.py | 100 +++++++++ homeassistant/components/tplink/switch.py | 201 ++++-------------- tests/components/tplink/consts.py | 72 +++++++ tests/components/tplink/test_init.py | 219 +++++++++++++++++++- tests/components/tplink/test_light.py | 2 +- 8 files changed, 634 insertions(+), 196 deletions(-) create mode 100644 homeassistant/components/tplink/sensor.py create mode 100644 tests/components/tplink/consts.py diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 1f843d364d8..9d5263687f8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -1,37 +1,57 @@ """Component to embed TP-Link smart home devices.""" -import logging +from __future__ import annotations +from datetime import datetime, timedelta +import logging +import time + +from pyHS100.smartdevice import SmartDevice, SmartDeviceException +from pyHS100.smartplug import SmartPlug import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_HOST, + CONF_MAC, + CONF_STATE, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import utc_from_timestamp -from .common import ( +from .common import SmartDevices, async_discover_devices, get_static_devices +from .const import ( ATTR_CONFIG, + ATTR_CURRENT_A, + ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, + CONF_EMETER_PARAMS, CONF_LIGHT, + CONF_MODEL, CONF_STRIP, + CONF_SW_VERSION, CONF_SWITCH, - SmartDevices, - async_discover_devices, - get_static_devices, + COORDINATORS, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) DOMAIN = "tplink" -PLATFORMS = [CONF_LIGHT, CONF_SWITCH] - TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -82,8 +102,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_count = len(tplink_devices) # These will contain the initialized devices - lights = hass.data[DOMAIN][CONF_LIGHT] = [] - switches = hass.data[DOMAIN][CONF_SWITCH] = [] + hass.data[DOMAIN][CONF_LIGHT] = [] + hass.data[DOMAIN][CONF_SWITCH] = [] + lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT] + switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH] # Add static devices static_devices = SmartDevices() @@ -102,14 +124,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: lights.extend(discovered_devices.lights) switches.extend(discovered_devices.switches) - forward_setup = hass.config_entries.async_forward_entry_setup if lights: _LOGGER.debug( "Got %s lights: %s", len(lights), ", ".join(d.host for d in lights) ) - hass.async_create_task(forward_setup(entry, "light")) - if switches: _LOGGER.debug( "Got %s switches: %s", @@ -117,7 +136,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ", ".join(d.host for d in switches), ) - hass.async_create_task(forward_setup(entry, "switch")) + # prepare DataUpdateCoordinators + hass.data[DOMAIN][COORDINATORS] = {} + for switch in switches: + + try: + await hass.async_add_executor_job(switch.get_sysinfo) + except SmartDeviceException as ex: + _LOGGER.debug(ex) + raise ConfigEntryNotReady from ex + + hass.data[DOMAIN][COORDINATORS][ + switch.mac + ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) + + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -130,3 +165,65 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].clear() return unload_ok + + +class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for specific SmartPlug.""" + + def __init__( + self, + hass: HomeAssistant, + smartplug: SmartPlug, + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" + self.smartplug = smartplug + + update_interval = timedelta(seconds=30) + super().__init__( + hass, _LOGGER, name=smartplug.alias, update_interval=update_interval + ) + + async def _async_update_data(self) -> dict: + """Fetch all device and sensor data from api.""" + info = self.smartplug.sys_info + data = { + CONF_HOST: self.smartplug.host, + CONF_MAC: info["mac"], + CONF_MODEL: info["model"], + CONF_SW_VERSION: info["sw_ver"], + } + if self.smartplug.context is None: + data[CONF_ALIAS] = info["alias"] + data[CONF_DEVICE_ID] = info["mac"] + data[CONF_STATE] = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + else: + plug_from_context = next( + c + for c in self.smartplug.sys_info["children"] + if c["id"] == self.smartplug.context + ) + data[CONF_ALIAS] = plug_from_context["alias"] + data[CONF_DEVICE_ID] = self.smartplug.context + data[CONF_STATE] = plug_from_context["state"] == 1 + if self.smartplug.has_emeter: + emeter_readings = self.smartplug.get_emeter_realtime() + data[CONF_EMETER_PARAMS] = { + ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), + ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), + ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), + ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), + ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + } + emeter_statics = self.smartplug.get_emeter_daily() + data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + if emeter_statics.get(int(time.strftime("%e"))): + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( + float(emeter_statics[int(time.strftime("%e"))]), 3 + ) + else: + # today's consumption not available, when device was off all the day + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + + return data diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 84ce7edadb6..6f6fb0a14c2 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -14,21 +14,20 @@ from pyHS100 import ( ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from .const import DOMAIN as TPLINK_DOMAIN +from .const import ( + CONF_DIMMER, + CONF_LIGHT, + CONF_STRIP, + CONF_SWITCH, + DOMAIN as TPLINK_DOMAIN, + MAX_DISCOVERY_RETRIES, +) _LOGGER = logging.getLogger(__name__) -ATTR_CONFIG = "config" -CONF_DIMMER = "dimmer" -CONF_DISCOVERY = "discovery" -CONF_LIGHT = "light" -CONF_STRIP = "strip" -CONF_SWITCH = "switch" -MAX_DISCOVERY_RETRIES = 4 - - class SmartDevices: """Hold different kinds of devices.""" @@ -98,7 +97,7 @@ async def async_discover_devices( else: _LOGGER.error("Unknown smart device type: %s", type(dev)) - devices = {} + devices: dict[str, SmartDevice] = {} for attempt in range(1, MAX_DISCOVERY_RETRIES + 1): _LOGGER.debug( "Discovering tplink devices, attempt %s of %s", @@ -159,16 +158,18 @@ def get_static_devices(config_data) -> SmartDevices: def add_available_devices( hass: HomeAssistant, device_type: str, device_class: Callable -) -> list: +) -> list[Entity]: """Get sysinfo for all devices.""" - devices = hass.data[TPLINK_DOMAIN][device_type] + devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][device_type] if f"{device_type}_remaining" in hass.data[TPLINK_DOMAIN]: - devices = hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"] + devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][ + f"{device_type}_remaining" + ] - entities_ready = [] - devices_unavailable = [] + entities_ready: list[Entity] = [] + devices_unavailable: list[SmartDevice] = [] for device in devices: try: device.get_sysinfo() diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 8b85b8afd74..93cad889a2f 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,5 +1,81 @@ """Const for TP-Link.""" +from __future__ import annotations + import datetime +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.const import ( + ATTR_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) + DOMAIN = "tplink" +COORDINATORS = "coordinators" + MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) +MAX_DISCOVERY_RETRIES = 4 + +ATTR_CONFIG = "config" +ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" +ATTR_CURRENT_A = "current_a" + +CONF_MODEL = "model" +CONF_SW_VERSION = "sw_ver" +CONF_EMETER_PARAMS = "emeter_params" +CONF_DIMMER = "dimmer" +CONF_DISCOVERY = "discovery" +CONF_LIGHT = "light" +CONF_STRIP = "strip" +CONF_SWITCH = "switch" +CONF_SENSOR = "sensor" + +PLATFORMS = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH] + +ENERGY_SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key=ATTR_CURRENT_POWER_W, + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + name="Current Consumption", + ), + SensorEntityDescription( + key=ATTR_TOTAL_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Total Consumption", + ), + SensorEntityDescription( + key=ATTR_TODAY_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Today's Consumption", + ), + SensorEntityDescription( + key=ATTR_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + name="Voltage", + ), + SensorEntityDescription( + key=ATTR_CURRENT_A, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + name="Current", + ), +] diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py new file mode 100644 index 00000000000..24b93e90963 --- /dev/null +++ b/homeassistant/components/tplink/sensor.py @@ -0,0 +1,100 @@ +"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" +from __future__ import annotations + +from typing import Any + +from pyHS100 import SmartPlug + +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC +from homeassistant.core import HomeAssistant +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONF_EMETER_PARAMS, + CONF_MODEL, + CONF_SW_VERSION, + CONF_SWITCH, + COORDINATORS, + DOMAIN as TPLINK_DOMAIN, + ENERGY_SENSORS, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + entities: list[SmartPlugSensor] = [] + coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][ + COORDINATORS + ] + switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] + for switch in switches: + coordinator: SmartPlugDataUpdateCoordinator = coordinators[switch.mac] + if not switch.has_emeter and coordinator.data.get(CONF_EMETER_PARAMS) is None: + continue + for description in ENERGY_SENSORS: + if coordinator.data[CONF_EMETER_PARAMS].get(description.key) is not None: + entities.append(SmartPlugSensor(switch, coordinator, description)) + + async_add_entities(entities) + + +class SmartPlugSensor(CoordinatorEntity, SensorEntity): + """Representation of a TPLink Smart Plug energy sensor.""" + + def __init__( + self, + smartplug: SmartPlug, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.smartplug = smartplug + self.entity_description = description + self._attr_name = f"{coordinator.data[CONF_ALIAS]} {description.name}" + self._attr_last_reset = coordinator.data[CONF_EMETER_PARAMS][ + ATTR_LAST_RESET + ].get(description.key) + + @property + def data(self) -> dict[str, Any]: + """Return data from DataUpdateCoordinator.""" + return self.coordinator.data + + @property + def state(self) -> float | None: + """Return the sensors state.""" + return self.data[CONF_EMETER_PARAMS][self.entity_description.key] + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{self.data[CONF_DEVICE_ID]}_{self.entity_description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return { + "name": self.data[CONF_ALIAS], + "model": self.data[CONF_MODEL], + "manufacturer": "TP-Link", + "connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])}, + "sw_version": self.data[CONF_SW_VERSION], + } diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index d088584c4ad..688091991c3 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,40 +1,30 @@ """Support for TPLink HS100/HS110/HS200 smart switch.""" from __future__ import annotations -import asyncio -from collections.abc import Mapping -from contextlib import suppress -import logging -import time from typing import Any -from pyHS100 import SmartDeviceException, SmartPlug +from pyHS100 import SmartPlug -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - SwitchEntity, -) +from homeassistant.components.switch import SwitchEntity +from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_VOLTAGE +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC, CONF_STATE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN -from .common import add_available_devices - -PARALLEL_UPDATES = 0 - -_LOGGER = logging.getLogger(__name__) - -ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" -ATTR_CURRENT_A = "current_a" - -MAX_ATTEMPTS = 300 -SLEEP_TIME = 2 +from .const import ( + CONF_MODEL, + CONF_SW_VERSION, + CONF_SWITCH, + COORDINATORS, + DOMAIN as TPLINK_DOMAIN, +) async def async_setup_entry( @@ -43,164 +33,65 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - entities = await hass.async_add_executor_job( - add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch - ) + entities: list[SmartPlugSwitch] = [] + coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][ + COORDINATORS + ] + switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] + for switch in switches: + coordinator = coordinators[switch.mac] + entities.append(SmartPlugSwitch(switch, coordinator)) - if entities: - async_add_entities(entities, update_before_add=True) - - if hass.data[TPLINK_DOMAIN][f"{CONF_SWITCH}_remaining"]: - raise PlatformNotReady + async_add_entities(entities) -class SmartPlugSwitch(SwitchEntity): +class SmartPlugSwitch(CoordinatorEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug: SmartPlug) -> None: + def __init__( + self, smartplug: SmartPlug, coordinator: DataUpdateCoordinator + ) -> None: """Initialize the switch.""" + super().__init__(coordinator) self.smartplug = smartplug - self._sysinfo = None - self._state = None - self._is_available = False - # Set up emeter cache - self._emeter_params = {} - self._mac = None - self._alias = None - self._model = None - self._device_id = None - self._host = None + @property + def data(self) -> dict[str, Any]: + """Return data from DataUpdateCoordinator.""" + return self.coordinator.data @property def unique_id(self) -> str | None: """Return a unique ID.""" - return self._device_id + return self.data[CONF_DEVICE_ID] @property def name(self) -> str | None: """Return the name of the Smart Plug.""" - return self._alias + return self.data[CONF_ALIAS] @property def device_info(self) -> DeviceInfo: """Return information about the device.""" return { - "name": self._alias, - "model": self._model, + "name": self.data[CONF_ALIAS], + "model": self.data[CONF_MODEL], "manufacturer": "TP-Link", - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, - "sw_version": self._sysinfo["sw_ver"], + "connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])}, + "sw_version": self.data[CONF_SW_VERSION], } - @property - def available(self) -> bool: - """Return if switch is available.""" - return self._is_available - @property def is_on(self) -> bool | None: """Return true if switch is on.""" - return self._state + return self.data[CONF_STATE] - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - self.smartplug.turn_on() + await self.hass.async_add_job(self.smartplug.turn_on) + await self.coordinator.async_refresh() - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - self.smartplug.turn_off() - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes of the device.""" - return self._emeter_params - - @property - def _plug_from_context(self) -> Any: - """Return the plug from the context.""" - children = self.smartplug.sys_info["children"] - return next(c for c in children if c["id"] == self.smartplug.context) - - def update_state(self) -> None: - """Update the TP-Link switch's state.""" - if self.smartplug.context is None: - self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON - else: - self._state = self._plug_from_context["state"] == 1 - - def attempt_update(self, update_attempt: int) -> bool: - """Attempt to get details from the TP-Link switch.""" - try: - if not self._sysinfo: - self._sysinfo = self.smartplug.sys_info - self._mac = self._sysinfo["mac"] - self._model = self._sysinfo["model"] - self._host = self.smartplug.host - if self.smartplug.context is None: - self._alias = self._sysinfo["alias"] - self._device_id = self._mac - else: - self._alias = self._plug_from_context["alias"] - self._device_id = self.smartplug.context - - self.update_state() - - if self.smartplug.has_emeter: - emeter_readings = self.smartplug.get_emeter_realtime() - - self._emeter_params[ATTR_CURRENT_POWER_W] = round( - float(emeter_readings["power"]), 2 - ) - self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = round( - float(emeter_readings["total"]), 3 - ) - self._emeter_params[ATTR_VOLTAGE] = round( - float(emeter_readings["voltage"]), 1 - ) - self._emeter_params[ATTR_CURRENT_A] = round( - float(emeter_readings["current"]), 2 - ) - - emeter_statics = self.smartplug.get_emeter_daily() - with suppress(KeyError): # Device returned no daily history - self._emeter_params[ATTR_TODAY_ENERGY_KWH] = round( - float(emeter_statics[int(time.strftime("%e"))]), 3 - ) - return True - except (SmartDeviceException, OSError) as ex: - if update_attempt == 0: - _LOGGER.debug( - "Retrying in %s seconds for %s|%s due to: %s", - SLEEP_TIME, - self._host, - self._alias, - ex, - ) - return False - - async def async_update(self) -> None: - """Update the TP-Link switch's state.""" - for update_attempt in range(MAX_ATTEMPTS): - is_ready = await self.hass.async_add_executor_job( - self.attempt_update, update_attempt - ) - - if is_ready: - self._is_available = True - if update_attempt > 0: - _LOGGER.debug( - "Device %s|%s responded after %s attempts", - self._host, - self._alias, - update_attempt, - ) - break - await asyncio.sleep(SLEEP_TIME) - - else: - if self._is_available: - _LOGGER.warning( - "Could not read state for %s|%s", self.smartplug.host, self._alias - ) - self._is_available = False + await self.hass.async_add_job(self.smartplug.turn_off) + await self.coordinator.async_refresh() diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py new file mode 100644 index 00000000000..de134ddbe07 --- /dev/null +++ b/tests/components/tplink/consts.py @@ -0,0 +1,72 @@ +"""Constants for the TP-Link component tests.""" + +SMARTPLUGSWITCH_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:ENE", + "mac": "69:F2:3C:8E:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": {"type": -1}, + "err_code": 0, + }, + "realtime": { + "voltage_mv": 233957, + "current_ma": 21, + "power_mw": 0, + "total_wh": 1793, + "err_code": 0, + }, +} +SMARTSTRIPWITCH_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM", + "mac": "69:F2:3C:8E:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": {"type": -1}, + "children": [{"id": "1", "state": 1, "alias": "SmartPlug#1"}], + "err_code": 0, + }, + "realtime": { + "voltage_mv": 233957, + "current_ma": 21, + "power_mw": 0, + "total_wh": 1793, + "err_code": 0, + }, + "context": "1", +} diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 49309a6ecef..0cfb4d3d233 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,24 +1,45 @@ """Tests for the TP-Link component.""" from __future__ import annotations +from datetime import datetime +import time from typing import Any from unittest.mock import MagicMock, patch -from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug +from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug, smartstrip +from pyHS100.smartdevice import EmeterStatus import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink -from homeassistant.components.tplink.common import ( +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.components.tplink.common import SmartDevices +from homeassistant.components.tplink.const import ( + ATTR_CURRENT_A, + ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, + CONF_EMETER_PARAMS, CONF_LIGHT, + CONF_MODEL, + CONF_SW_VERSION, CONF_SWITCH, + COORDINATORS, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_HOST, + CONF_MAC, +) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utc_from_timestamp from tests.common import MockConfigEntry, mock_coro +from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA async def test_creating_entry_tries_discover(hass): @@ -186,7 +207,7 @@ async def test_configuring_discovery_disabled(hass): assert mock_setup.call_count == 1 -async def test_platforms_are_initialized(hass): +async def test_platforms_are_initialized(hass: HomeAssistant): """Test that platforms are initialized per configuration array.""" config = { tplink.DOMAIN: { @@ -199,6 +220,8 @@ async def test_platforms_are_initialized(hass): with patch( "homeassistant.components.tplink.common.Discover.discover" ) as discover, patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", @@ -209,13 +232,141 @@ async def test_platforms_are_initialized(hass): ) as switch_setup, patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): + + light = SmartBulb("123.123.123.123") + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_emeter_realtime = MagicMock( + return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + ) + switch.get_emeter_daily = MagicMock( + return_value={int(time.strftime("%e")): 1.123} + ) + get_static_devices.return_value = SmartDevices([light], [switch]) + # patching is_dimmable is necessray to avoid misdetection as light. await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert discover.call_count == 0 - assert light_setup.call_count == 1 - assert switch_setup.call_count == 1 + assert hass.data.get(tplink.DOMAIN) + assert hass.data[tplink.DOMAIN].get(COORDINATORS) + assert hass.data[tplink.DOMAIN][COORDINATORS].get(switch.mac) + assert isinstance( + hass.data[tplink.DOMAIN][COORDINATORS][switch.mac], + tplink.SmartPlugDataUpdateCoordinator, + ) + data = hass.data[tplink.DOMAIN][COORDINATORS][switch.mac].data + assert data[CONF_HOST] == switch.host + assert data[CONF_MAC] == switch.sys_info["mac"] + assert data[CONF_MODEL] == switch.sys_info["model"] + assert data[CONF_SW_VERSION] == switch.sys_info["sw_ver"] + assert data[CONF_ALIAS] == switch.sys_info["alias"] + assert data[CONF_DEVICE_ID] == switch.sys_info["mac"] + + emeter_readings = switch.get_emeter_realtime() + assert data[CONF_EMETER_PARAMS][ATTR_VOLTAGE] == round( + float(emeter_readings["voltage"]), 1 + ) + assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_A] == round( + float(emeter_readings["current"]), 2 + ) + assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_POWER_W] == round( + float(emeter_readings["power"]), 2 + ) + assert data[CONF_EMETER_PARAMS][ATTR_TOTAL_ENERGY_KWH] == round( + float(emeter_readings["total"]), 3 + ) + assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TOTAL_ENERGY_KWH + ] == utc_from_timestamp(0) + + assert data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] == 1.123 + assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] == datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + + assert discover.call_count == 0 + assert get_static_devices.call_count == 1 + assert light_setup.call_count == 1 + assert switch_setup.call_count == 1 + + +async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): + """Test that platforms are initialized per configuration array.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}], + } + } + + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.sensor.SmartPlugSensor.__init__" + ) as SmartPlugSensor, patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ): + + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + get_static_devices.return_value = SmartDevices([], [switch]) + + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert SmartPlugSensor.call_count == 0 + + +async def test_smartstrip_device(hass: HomeAssistant): + """Test discover a SmartStrip devices.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: True, + } + } + + class SmartStrip(smartstrip.SmartStrip): + """Moked SmartStrip class.""" + + def get_sysinfo(self): + return SMARTSTRIPWITCH_DATA["sysinfo"] + + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.common.SmartPlug.get_sysinfo", + return_value=SMARTSTRIPWITCH_DATA["sysinfo"], + ), patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + ): + + strip = SmartStrip("123.123.123.123") + discover.return_value = {"123.123.123.123": strip} + + assert await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert hass.data.get(tplink.DOMAIN) + assert hass.data[tplink.DOMAIN].get(COORDINATORS) + assert hass.data[tplink.DOMAIN][COORDINATORS].get(strip.mac) + assert isinstance( + hass.data[tplink.DOMAIN][COORDINATORS][strip.mac], + tplink.SmartPlugDataUpdateCoordinator, + ) + data = hass.data[tplink.DOMAIN][COORDINATORS][strip.mac].data + assert data[CONF_ALIAS] == strip.sys_info["children"][0]["alias"] + assert data[CONF_DEVICE_ID] == "1" async def test_no_config_creates_no_entry(hass): @@ -230,6 +381,42 @@ async def test_no_config_creates_no_entry(hass): assert mock_setup.call_count == 0 +async def test_not_ready(hass: HomeAssistant): + """Test for not ready when configured devices are not available.""" + config = { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}], + } + } + + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ): + + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) + get_static_devices.return_value = SmartDevices([], [switch]) + + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY + + @pytest.mark.parametrize("platform", ["switch", "light"]) async def test_unload(hass, platform): """Test that the async_unload_entry works.""" @@ -238,21 +425,35 @@ async def test_unload(hass, platform): entry.add_to_hass(hass) with patch( + "homeassistant.components.tplink.get_static_devices" + ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( f"homeassistant.components.tplink.{platform}.async_setup_entry", return_value=mock_coro(True), - ) as light_setup: + ) as async_setup_entry: config = { tplink.DOMAIN: { platform: [{CONF_HOST: "123.123.123.123"}], CONF_DISCOVERY: False, } } + + light = SmartBulb("123.123.123.123") + switch = SmartPlug("321.321.321.321") + switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_emeter_realtime = MagicMock( + return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + ) + if platform == "light": + get_static_devices.return_value = SmartDevices([light], []) + elif platform == "switch": + get_static_devices.return_value = SmartDevices([], [switch]) + assert await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert len(light_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 assert tplink.DOMAIN in hass.data assert await tplink.async_unload_entry(hass, entry) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index ea8809bc679..c9b07529ea4 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.tplink.common import ( +from homeassistant.components.tplink.const import ( CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, From 7e6856ace832c6d2ca49ad499a715d1db415c344 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Jul 2021 03:30:50 -0400 Subject: [PATCH 012/199] Add enabled attribute to zwave_js discovery model (#53645) * Add attribute to zwave_js discovery model * Fix binary sensor entity enabled logic * Add tests --- .../components/zwave_js/binary_sensor.py | 6 -- .../components/zwave_js/discovery.py | 55 +++++++++++++++++-- homeassistant/components/zwave_js/entity.py | 3 + homeassistant/components/zwave_js/sensor.py | 9 --- tests/components/zwave_js/common.py | 2 + tests/components/zwave_js/test_sensor.py | 24 ++++++++ 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 71de7270d9a..9d72a804ca0 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -277,12 +277,6 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): if self.info.primary_value.command_class == CommandClass.BATTERY else None ) - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - self._attr_entity_registry_enabled_default = bool( - self.info.primary_value.command_class != CommandClass.SENSOR_BINARY - or self.info.node.device_class.generic.key == 0x20 - ) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 403ea5c9746..7c29c89dfab 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -67,6 +67,8 @@ class ZwaveDiscoveryInfo: platform_data: dict[str, Any] | None = None # additional values that need to be watched by entity additional_value_ids_to_watch: set[str] | None = None + # bool to specify whether entity should be enabled by default + entity_registry_enabled_default: bool = True @dataclass @@ -135,6 +137,8 @@ class ZWaveDiscoverySchema: allow_multi: bool = False # [optional] bool to specify whether state is assumed and events should be fired on value update assumed_state: bool = False + # [optional] bool to specify whether entity should be enabled by default + entity_registry_enabled_default: bool = True def get_config_parameter_discovery_schema( @@ -161,6 +165,7 @@ def get_config_parameter_discovery_schema( property_key_name=property_key_name, type={"number"}, ), + entity_registry_enabled_default=False, **kwargs, ) @@ -428,12 +433,33 @@ DISCOVERY_SCHEMAS = [ ], ), # binary sensors + # When CC is Sensor Binary and device class generic is Binary Sensor, entity should + # be enabled by default + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="boolean", + device_class_generic={"Binary Sensor"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SENSOR_BINARY}, + type={"boolean"}, + ), + ), + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="boolean", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SENSOR_BINARY}, + type={"boolean"}, + ), + entity_registry_enabled_default=False, + ), ZWaveDiscoverySchema( platform="binary_sensor", hint="boolean", primary_value=ZWaveValueDiscoverySchema( command_class={ - CommandClass.SENSOR_BINARY, CommandClass.BATTERY, CommandClass.SENSOR_ALARM, }, @@ -456,13 +482,19 @@ DISCOVERY_SCHEMAS = [ platform="sensor", hint="string_sensor", primary_value=ZWaveValueDiscoverySchema( - command_class={ - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - }, + command_class={CommandClass.SENSOR_ALARM}, type={"string"}, ), ), + ZWaveDiscoverySchema( + platform="sensor", + hint="string_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.INDICATOR}, + type={"string"}, + ), + entity_registry_enabled_default=False, + ), # generic numeric sensors ZWaveDiscoverySchema( platform="sensor", @@ -471,12 +503,20 @@ DISCOVERY_SCHEMAS = [ command_class={ CommandClass.SENSOR_MULTILEVEL, CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, CommandClass.BATTERY, }, type={"number"}, ), ), + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.INDICATOR}, + type={"number"}, + ), + entity_registry_enabled_default=False, + ), # numeric sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", @@ -500,6 +540,7 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), allow_multi=True, + entity_registry_enabled_default=False, ), # sensor for basic CC ZWaveDiscoverySchema( @@ -512,6 +553,7 @@ DISCOVERY_SCHEMAS = [ type={"number"}, property={"currentValue"}, ), + entity_registry_enabled_default=False, ), # binary switches ZWaveDiscoverySchema( @@ -697,6 +739,7 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None platform_data_template=schema.data_template, platform_data=resolved_data, additional_value_ids_to_watch=additional_value_ids_to_watch, + entity_registry_enabled_default=schema.entity_registry_enabled_default, ) if not schema.allow_multi: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 6df7c8d546b..793eaa435d5 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -46,6 +46,9 @@ class ZWaveBaseEntity(Entity): self._attr_unique_id = get_unique_id( self.client.driver.controller.home_id, self.info.primary_value.value_id ) + self._attr_entity_registry_enabled_default = ( + self.info.entity_registry_enabled_default + ) self._attr_assumed_state = self.info.assumed_state # device is precreated in main handler self._attr_device_info = { diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 7c41ad035be..209a5b6d4aa 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -118,15 +118,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): self._attr_name = self.generate_name(include_value_name=True) self._attr_device_class = self._get_device_class() self._attr_state_class = self._get_state_class() - self._attr_entity_registry_enabled_default = True - # We hide some of the more advanced sensors by default to not overwhelm users - if self.info.primary_value.command_class in [ - CommandClass.BASIC, - CommandClass.CONFIGURATION, - CommandClass.INDICATOR, - CommandClass.NOTIFICATION, - ]: - self._attr_entity_registry_enabled_default = False def _get_device_class(self) -> str | None: """ diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index f14842609c5..7177134aa33 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -11,6 +11,8 @@ NOTIFICATION_MOTION_BINARY_SENSOR = ( "binary_sensor.multisensor_6_home_security_motion_detection" ) NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" +INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value" +BASIC_SENSOR = "sensor.livingroomlight_basic" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index e368ec1b026..aae7a1c0602 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -21,9 +21,11 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, + BASIC_SENSOR, ENERGY_SENSOR, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, + INDICATOR_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, ) @@ -88,6 +90,28 @@ async def test_disabled_notification_sensor(hass, multisensor_6, integration): assert state.attributes["value"] == 8 +async def test_disabled_indcator_sensor( + hass, climate_radio_thermostat_ct100_plus, integration +): + """Test sensor is created from Indicator CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(INDICATOR_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + +async def test_disabled_basic_sensor(hass, ge_in_wall_dimmer_switch, integration): + """Test sensor is created from Basic CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(BASIC_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + + async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration): """Test config parameter sensor is created.""" ent_reg = er.async_get(hass) From b3367d8b3fbbc166faa82575b93ea5707744c71d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 29 Jul 2021 06:15:28 +0100 Subject: [PATCH 013/199] Prosegur code quality improvements (#53647) --- homeassistant/components/prosegur/__init__.py | 11 +++----- .../components/prosegur/config_flow.py | 13 ++++------ tests/components/prosegur/common.py | 2 +- .../prosegur/test_alarm_control_panel.py | 6 ++--- tests/components/prosegur/test_config_flow.py | 26 +++---------------- tests/components/prosegur/test_init.py | 2 -- 6 files changed, 15 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index 2e4a0bd0ed5..3e31a1142ce 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -3,10 +3,10 @@ import logging from pyprosegur.auth import Auth -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import CONF_COUNTRY, DOMAIN @@ -32,12 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except ConnectionRefusedError as error: _LOGGER.error("Configured credential are invalid, %s", error) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": entry.data["entry_id"]}, - ) - ) + raise ConfigEntryAuthFailed from error except ConnectionError as error: _LOGGER.error("Could not connect with Prosegur backend: %s", error) diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index af1ae456f12..1807561663b 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -25,11 +25,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" + session = aiohttp_client.async_get_clientsession(hass) + auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]) try: - session = aiohttp_client.async_get_clientsession(hass) - auth = Auth( - session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY] - ) install = await Installation.retrieve(auth) except ConnectionRefusedError: raise InvalidAuth from ConnectionRefusedError @@ -95,15 +93,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - data = self.entry.data.copy() self.hass.config_entries.async_update_entry( self.entry, data={ - **data, + **self.entry.data, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/tests/components/prosegur/common.py b/tests/components/prosegur/common.py index 504da3ea92a..bed9d987ceb 100644 --- a/tests/components/prosegur/common.py +++ b/tests/components/prosegur/common.py @@ -8,7 +8,7 @@ from tests.common import MockConfigEntry CONTRACT = "1234abcd" -async def setup_platform(hass, platform): +async def setup_platform(hass): """Set up the Prosegur platform.""" mock_entry = MockConfigEntry( domain=PROSEGUR_DOMAIN, diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 26e0c5f94b3..9ab0c0d37de 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -48,7 +48,7 @@ def mock_status(request): async def test_entity_registry(hass, mock_auth, mock_status): """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY) @@ -74,7 +74,7 @@ async def test_connection_error(hass, mock_auth): with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) await hass.async_block_till_done() @@ -106,7 +106,7 @@ async def test_arm(hass, mock_auth, code, alarm_service, alarm_state): install.status = code with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass, ALARM_DOMAIN) + await setup_platform(hass) await hass.services.async_call( ALARM_DOMAIN, diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 447baefed23..bece0bae621 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass): with patch( "homeassistant.components.prosegur.config_flow.Installation.retrieve", return_value=install, - ), patch( + ) as mock_retrieve, patch( "homeassistant.components.prosegur.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -50,6 +50,8 @@ async def test_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_retrieve.mock_calls) == 1 + async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" @@ -120,28 +122,6 @@ async def test_form_unknown_exception(hass): assert result2["errors"] == {"base": "unknown"} -async def test_form_validate_input(hass): - """Test we retrieve data from Installation.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyprosegur.installation.Installation.retrieve", - return_value=MagicMock, - ) as mock_retrieve: - await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "country": "PT", - }, - ) - - assert len(mock_retrieve.mock_calls) == 1 - - async def test_reauth_flow(hass): """Test a reauthentication flow.""" entry = MockConfigEntry( diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py index e0fe596ee13..2079d7a2b3c 100644 --- a/tests/components/prosegur/test_init.py +++ b/tests/components/prosegur/test_init.py @@ -18,7 +18,6 @@ from tests.common import MockConfigEntry async def test_setup_entry_fail_retrieve(hass, error): """Test loading the Prosegur entry.""" - hass.config.components.add(DOMAIN) config_entry = MockConfigEntry( domain=DOMAIN, data={ @@ -47,7 +46,6 @@ async def test_unload_entry(hass, aioclient_mock): json={"data": {"token": "123456789"}}, ) - hass.config.components.add(DOMAIN) config_entry = MockConfigEntry( domain=DOMAIN, data={ From 2aeecba64c3dcc329b6ef71f344e2f052f607e17 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Jul 2021 23:16:14 -0600 Subject: [PATCH 014/199] Fix unhandled exception with Guardian paired sensor coordinators (#53663) --- homeassistant/components/guardian/__init__.py | 17 +++++++++-------- .../components/guardian/binary_sensor.py | 12 ++++++------ homeassistant/components/guardian/const.py | 1 + homeassistant/components/guardian/sensor.py | 12 ++++++------ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 9338f9a47a9..915746c5ed5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -26,6 +26,7 @@ from .const import ( CONF_UID, DATA_CLIENT, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_PAIRED_SENSOR_MANAGER, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, @@ -44,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: { DATA_CLIENT: {}, DATA_COORDINATOR: {}, + DATA_COORDINATOR_PAIRED_SENSOR: {}, DATA_PAIRED_SENSOR_MANAGER: {}, DATA_UNSUB_DISPATCHER_CONNECT: {}, }, @@ -51,9 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = { - API_SENSOR_PAIRED_SENSOR_STATUS: {} - } + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id] = [] # The valve controller's UDP-based API can't handle concurrent requests very well, @@ -113,6 +114,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR].pop(entry.entry_id) for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]: unsub() hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id) @@ -143,8 +145,8 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + self._entry.entry_id ][uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, @@ -194,8 +196,8 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + self._entry.entry_id ].pop(uid) # Remove the paired sensor device from the device registry (which will @@ -297,7 +299,6 @@ class ValveControllerEntity(GuardianEntity): return any( coordinator.last_update_success for coordinator in self.coordinators.values() - if coordinator ) async def _async_continue_entity_setup(self) -> None: diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 8ce381e0456..1cbc9f5cede 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -15,11 +15,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity from .const import ( - API_SENSOR_PAIRED_SENSOR_STATUS, API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, CONF_UID, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -49,9 +49,9 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS - ][uid] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + uid + ] entities = [] for kind in PAIRED_SENSOR_SENSORS: @@ -95,8 +95,8 @@ async def async_setup_entry( ) # Add all paired sensor-specific binary sensors: - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + entry.entry_id ].values(): for kind in PAIRED_SENSOR_SENSORS: name, device_class = SENSOR_ATTRS_MAP[kind] diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 750a8c407ca..e27e8a37047 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -16,6 +16,7 @@ CONF_UID = "uid" DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" +DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" DATA_UNSUB_DISPATCHER_CONNECT = "unsub_dispatcher_connect" diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index ce09bb99c60..2d7cde86cca 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -17,11 +17,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PairedSensorEntity, ValveControllerEntity from .const import ( - API_SENSOR_PAIRED_SENSOR_STATUS, API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, DATA_COORDINATOR, + DATA_COORDINATOR_PAIRED_SENSOR, DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -54,9 +54,9 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS - ][uid] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + uid + ] entities = [] for kind in PAIRED_SENSOR_SENSORS: @@ -96,8 +96,8 @@ async def async_setup_entry( ) # Add all paired sensor-specific binary sensors: - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - API_SENSOR_PAIRED_SENSOR_STATUS + for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ + entry.entry_id ].values(): for kind in PAIRED_SENSOR_SENSORS: name, device_class, icon, unit = SENSOR_ATTRS_MAP[kind] From d19d487b211d50a4fee5025c13774edb71c56009 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Jul 2021 08:18:38 -0400 Subject: [PATCH 015/199] Add energy support for zwave_js meter CC entities (#53665) * Add energy support for zwave_js meter CC entities * shrink * comments * comments * comments * Move attributes * Add tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 4 +- homeassistant/components/zwave_js/sensor.py | 85 +++++++++++++++++-- tests/components/zwave_js/common.py | 6 ++ tests/components/zwave_js/conftest.py | 18 ++++ tests/components/zwave_js/test_sensor.py | 52 +++++++++--- 5 files changed, 146 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7c29c89dfab..588b4c76472 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -517,10 +517,10 @@ DISCOVERY_SCHEMAS = [ ), entity_registry_enabled_default=False, ), - # numeric sensors for Meter CC + # Meter sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", - hint="numeric_sensor", + hint="meter", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.METER, diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 209a5b6d4aa..39aa2f30604 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -11,6 +11,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ConfigurationValue from homeassistant.components.sensor import ( + ATTR_LAST_RESET, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, @@ -28,8 +29,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER from .discovery import ZwaveDiscoveryInfo @@ -60,6 +66,8 @@ async def async_setup_entry( entities.append(ZWaveListSensor(config_entry, client, info)) elif info.platform_hint == "config_parameter": entities.append(ZWaveConfigParameterSensor(config_entry, client, info)) + elif info.platform_hint == "meter": + entities.append(ZWaveMeterSensor(config_entry, client, info)) else: LOGGER.warning( "Sensor not implemented for %s/%s", @@ -128,10 +136,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """ if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY - if self.info.primary_value.command_class == CommandClass.METER: - if self.info.primary_value.metadata.unit == "kWh": - return DEVICE_CLASS_ENERGY - return DEVICE_CLASS_POWER if isinstance(self.info.primary_value.property_, str): property_lower = self.info.primary_value.property_.lower() if "humidity" in property_lower: @@ -221,14 +225,72 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) + +class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): + """Representation of a Z-Wave Meter CC sensor.""" + + _attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveNumericSensor entity.""" + super().__init__(config_entry, client, info) + + # Entity class attributes + self._attr_last_reset = dt.utc_from_timestamp(0) + self._attr_device_class = DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "kWh": + self._attr_device_class = DEVICE_CLASS_ENERGY + + @callback + def async_update_last_reset( + self, node: ZwaveNode, endpoint: int, meter_type: int | None + ) -> None: + """Update last reset.""" + # If the signal is not for this node or is for a different endpoint, ignore it + if self.info.node != node or self.info.primary_value.endpoint != endpoint: + return + # If a meter type was specified and doesn't match this entity's meter type, + # ignore it + if ( + meter_type is not None + and self.info.primary_value.metadata.cc_specific.get("meterType") + != meter_type + ): + return + + self._attr_last_reset = dt.utcnow() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + await super().async_added_to_hass() + + # Restore the last reset time from stored state + restored_state = await self.async_get_last_state() + if restored_state and ATTR_LAST_RESET in restored_state.attributes: + self._attr_last_reset = dt.parse_datetime( + restored_state.attributes[ATTR_LAST_RESET] + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{SERVICE_RESET_METER}", + self.async_update_last_reset, + ) + ) + async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None ) -> None: """Reset meter(s) on device.""" node = self.info.node primary_value = self.info.primary_value - if primary_value.command_class != CommandClass.METER: - raise TypeError("Reset only available for Meter sensors") options = {} if meter_type is not None: options["type"] = meter_type @@ -244,6 +306,15 @@ class ZWaveNumericSensor(ZwaveSensorBase): primary_value.endpoint, options, ) + self._attr_last_reset = dt.utcnow() + # Notify meters that may have been reset + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{SERVICE_RESET_METER}", + node, + primary_value.endpoint, + options.get("type"), + ) class ZWaveListSensor(ZwaveSensorBase): diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 7177134aa33..c7100b22bd5 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,4 +1,6 @@ """Provide common test tools for Z-Wave JS.""" +from datetime import datetime, timezone + AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" @@ -29,3 +31,7 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" ) ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" +METER_SENSOR = "sensor.smart_switch_6_electric_consumed_v" + +DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc) +DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 9dc8490b314..75b5ab65d38 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -11,6 +11,11 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo +from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.core import State + +from .common import DATETIME_LAST_RESET + from tests.common import MockConfigEntry, load_fixture # Add-on fixtures @@ -835,3 +840,16 @@ def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): def firmware_file_fixture(): """Return mock firmware file stream.""" return io.BytesIO(bytes(10)) + + +@pytest.fixture(name="restore_last_reset") +def restore_last_reset_fixture(): + """Return mock restore last reset.""" + state = State( + "sensor.test", "test", {ATTR_LAST_RESET: DATETIME_LAST_RESET.isoformat()} + ) + with patch( + "homeassistant.components.zwave_js.sensor.ZWaveMeterSensor.async_get_last_state", + return_value=state, + ): + yield state diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index aae7a1c0602..29fed2b5c55 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,6 +1,9 @@ """Test the Z-Wave JS sensor platform.""" +from unittest.mock import patch + from zwave_js_server.event import Event +from homeassistant.components.sensor import ATTR_LAST_RESET from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -22,10 +25,13 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, BASIC_SENSOR, + DATETIME_LAST_RESET, + DATETIME_ZERO, ENERGY_SENSOR, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, + METER_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, ) @@ -171,18 +177,30 @@ async def test_reset_meter( integration, ): """Test reset_meter service.""" - SENSOR = "sensor.smart_switch_6_electric_consumed_v" client.async_send_command.return_value = {} client.async_send_command_no_wait.return_value = {} - # Test successful meter reset call - await hass.services.async_call( - DOMAIN, - SERVICE_RESET_METER, - { - ATTR_ENTITY_ID: SENSOR, - }, - blocking=True, + # Validate that the sensor last reset is starting from nothing + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_ZERO.isoformat() + ) + + # Test successful meter reset call, patching utcnow so we can make sure the last + # reset gets updated + with patch("homeassistant.util.dt.utcnow", return_value=DATETIME_LAST_RESET): + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: METER_SENSOR, + }, + blocking=True, + ) + + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_LAST_RESET.isoformat() ) assert len(client.async_send_command_no_wait.call_args_list) == 1 @@ -199,7 +217,7 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: SENSOR, + ATTR_ENTITY_ID: METER_SENSOR, ATTR_METER_TYPE: 1, ATTR_VALUE: 2, }, @@ -214,3 +232,17 @@ async def test_reset_meter( assert args["args"] == [{"type": 1, "targetValue": 2}] client.async_send_command_no_wait.reset_mock() + + +async def test_restore_last_reset( + hass, + client, + aeon_smart_switch_6, + restore_last_reset, + integration, +): + """Test restoring last_reset on setup.""" + assert ( + hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + == DATETIME_LAST_RESET.isoformat() + ) From db8aa4658aaac4578483a26020447731f25608f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jul 2021 11:07:52 -0500 Subject: [PATCH 016/199] Skip each ssdp listener that fails to bind (#53670) --- homeassistant/components/ssdp/__init__.py | 16 +++++- tests/components/ssdp/test_init.py | 62 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index f0ba8b7dcea..31ebb0d1a92 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -256,9 +256,21 @@ class Scanner: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start ) - await asyncio.gather( - *(listener.async_start() for listener in self._ssdp_listeners) + results = await asyncio.gather( + *(listener.async_start() for listener in self._ssdp_listeners), + return_exceptions=True, ) + failed_listeners = [] + for idx, result in enumerate(results): + if isinstance(result, Exception): + _LOGGER.warning( + "Failed to setup listener for %s: %s", + self._ssdp_listeners[idx].source_ip, + result, + ) + failed_listeners.append(self._ssdp_listeners[idx]) + for listener in failed_listeners: + self._ssdp_listeners.remove(listener) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL ) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 568a2261fee..34ca1b7228e 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -790,3 +790,65 @@ async def test_async_detect_interfaces_setting_empty_route(hass): (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), (IPv4Address("192.168.1.5"), None), } + + +async def test_bind_failure_skips_adapter(hass, caplog): + """Test that an adapter with a bind failure is skipped.""" + mock_get_ssdp = { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + } + create_args = [] + did_search = 0 + + @callback + def _callback(*_): + nonlocal did_search + did_search += 1 + pass + + def _generate_failing_ssdp_listener(*args, **kwargs): + create_args.append([args, kwargs]) + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + if kwargs["source_ip"] == IPv6Address("2001:db8::"): + raise OSError + pass + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_failing_ssdp_listener, + ), patch( + "homeassistant.components.ssdp.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ): + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + argset = set() + for argmap in create_args: + argset.add((argmap[1].get("source_ip"), argmap[1].get("target_ip"))) + + assert argset == { + (IPv6Address("2001:db8::"), None), + (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), + (IPv4Address("192.168.1.5"), None), + } + assert "Failed to setup listener for" in caplog.text + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert did_search == 2 From d7768f13c1b4191027663c301dd22dce3f652944 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 29 Jul 2021 00:13:55 -0700 Subject: [PATCH 017/199] pyWeMo version bump (0.6.6) (#53671) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 3d051fcc6dc..21a7760741a 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.5"], + "requirements": ["pywemo==0.6.6"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 6f9381d19ca..0538ecc5180 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1977,7 +1977,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.5 +pywemo==0.6.6 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1f1ba01960..1d3d9d9d070 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1095,7 +1095,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.5 +pywemo==0.6.6 # homeassistant.components.wilight pywilight==0.0.70 From 268f0dd62f77ee660df3ac7edb6e4eade7e44a59 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Jul 2021 22:49:13 -0700 Subject: [PATCH 018/199] Bump nest to version 0.3.5 (#53672) Fix runtime type assertions, Fixes Issue #53652 --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 05cfa261ef4..6c9462e43db 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.4"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.5"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0538ecc5180..063d8d0b4fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,7 +707,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.4 +google-nest-sdm==0.3.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d3d9d9d070..062e5068c91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.4 +google-nest-sdm==0.3.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 43a89dc4522851aa85967b5eecf1c1bc8503c090 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Jul 2021 21:09:14 +0200 Subject: [PATCH 019/199] Fix `last_reset_topic` config replaces `state_topic` for sensor platform (#53677) --- homeassistant/components/mqtt/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0c234fbbbea..239af7b450a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -196,7 +196,7 @@ class MqttSensor(MqttEntity, SensorEntity): self.async_write_ha_state() if CONF_LAST_RESET_TOPIC in self._config: - topics["state_topic"] = { + topics["last_reset_topic"] = { "topic": self._config[CONF_LAST_RESET_TOPIC], "msg_callback": last_reset_message_received, "qos": self._config[CONF_QOS], From 9ad29ae75cac2500af655e7eeaf0ae29a20b5a03 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Jul 2021 21:08:53 +0200 Subject: [PATCH 020/199] Only disable a device if all associated config entries are disabled (#53681) --- homeassistant/helpers/device_registry.py | 11 +++++++ tests/helpers/test_device_registry.py | 42 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9f09bbbf642..b22b1740a4f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -670,6 +670,7 @@ def async_config_entry_disabled_by_changed( the config entry is disabled, enable devices in the registry that are associated with a config entry when the config entry is enabled and the devices are marked DISABLED_CONFIG_ENTRY. + Only disable a device if all associated config entries are disabled. """ devices = async_entries_for_config_entry(registry, config_entry.entry_id) @@ -681,10 +682,20 @@ def async_config_entry_disabled_by_changed( registry.async_update_device(device.id, disabled_by=None) return + enabled_config_entries = { + entry.entry_id + for entry in registry.hass.config_entries.async_entries() + if not entry.disabled_by + } + for device in devices: if device.disabled: # Device already disabled, do not overwrite continue + if len(device.config_entries) > 1 and device.config_entries.intersection( + enabled_config_entries + ): + continue registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 037e1aec8c2..557647c5c7f 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1253,3 +1253,45 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry2 = registry.async_get(entry2.id) assert entry2.disabled assert entry2.disabled_by == device_registry.DISABLED_USER + + +async def test_only_disable_device_if_all_config_entries_are_disabled(hass, registry): + """Test that we only disable device if all related config entries are disabled.""" + config_entry1 = MockConfigEntry(domain="light") + config_entry1.add_to_hass(hass) + config_entry2 = MockConfigEntry(domain="light") + config_entry2.add_to_hass(hass) + + registry.async_get_or_create( + config_entry_id=config_entry1.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry1 = registry.async_get_or_create( + config_entry_id=config_entry2.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert len(entry1.config_entries) == 2 + assert not entry1.disabled + + await hass.config_entries.async_set_disabled_by( + config_entry1.entry_id, config_entries.DISABLED_USER + ) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert not entry1.disabled + + await hass.config_entries.async_set_disabled_by( + config_entry2.entry_id, config_entries.DISABLED_USER + ) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert entry1.disabled + assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY + + await hass.config_entries.async_set_disabled_by(config_entry1.entry_id, None) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert not entry1.disabled From 7f314e17de58e8032ea42b893745359adfb36328 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 29 Jul 2021 20:57:46 +0200 Subject: [PATCH 021/199] Bump bimmer_connected to 0.7.16 to fix parking light issue (#53687) --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index aff9e4fd647..17aaa166942 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.15"], + "requirements": ["bimmer_connected==0.7.16"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 063d8d0b4fd..e9a46b9e079 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -367,7 +367,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.15 +bimmer_connected==0.7.16 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 062e5068c91..29938321ee3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.15 +bimmer_connected==0.7.16 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 6ced0153df57ab218b133a4cc304fece958a4deb Mon Sep 17 00:00:00 2001 From: Andrew55529 Date: Thu, 29 Jul 2021 19:31:32 +0300 Subject: [PATCH 022/199] Fix problem with telegram_bot (#53690) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index c3f07e5269d..02629e695fc 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -848,7 +848,7 @@ class BaseTelegramBotEntity: if ( msg_data["from"].get("id") not in self.allowed_chat_ids - and msg_data["chat"].get("id") not in self.allowed_chat_ids + and msg_data["message"]["chat"].get("id") not in self.allowed_chat_ids ): # Neither from id nor chat id was in allowed_chat_ids, # origin is not allowed. From 1117158bd0442cfd6012044dc606305826487933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 16:59:02 +0200 Subject: [PATCH 023/199] Surepetcare, bug fix (#53695) --- homeassistant/components/surepetcare/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index ca7b7378127..5a9ae733db0 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -175,7 +175,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Get the latest data and update the state.""" surepy_entity = self._spc.states[self._id] state = surepy_entity.raw_data()["status"] - self._attr_is_on = self._attr_available = bool(self.state) + self._attr_is_on = self._attr_available = bool(state) if state: self._attr_extra_state_attributes = { "device_rssi": f'{state["signal"]["device_rssi"]:.2f}', From aa179a1ad99bcf918b2e05eb3006b18edc9d59c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 18:44:38 +0200 Subject: [PATCH 024/199] Energy round (#53696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Energy. Round cost Signed-off-by: Daniel Hjelseth Høyer * Energy. Round cost Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/energy/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/energy/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 773abbbe6b9..1c42ea5a050 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -152,10 +152,12 @@ class EnergyCostSensor(SensorEntity): self._attr_state_class = STATE_CLASS_MEASUREMENT self._flow = flow self._last_energy_sensor_state: State | None = None + self._cur_value = 0.0 def _reset(self, energy_state: State) -> None: """Reset the cost sensor.""" self._attr_state = 0.0 + self._cur_value = 0.0 self._attr_last_reset = dt_util.utcnow() self._last_energy_sensor_state = energy_state self.async_write_ha_state() @@ -195,7 +197,6 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state) return - cur_value = cast(float, self._attr_state) if ( energy_state.attributes[ATTR_LAST_RESET] != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] @@ -205,7 +206,8 @@ class EnergyCostSensor(SensorEntity): else: # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) - self._attr_state = cur_value + (energy - old_energy_value) * energy_price + self._cur_value += (energy - old_energy_value) * energy_price + self._attr_state = round(self._cur_value, 2) self._last_energy_sensor_state = energy_state From 462e3a3d0d2d65a18904dc16dc57c0420ee961d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 29 Jul 2021 20:05:53 +0200 Subject: [PATCH 025/199] Integration. Add device class, last_reset, state_class (#53698) Co-authored-by: Franck Nijhof --- .../components/integration/sensor.py | 29 ++++- tests/components/integration/test_sensor.py | 116 ++++++++++++++++-- 2 files changed, 132 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 0ab4ac0d2c4..dea8970f4f7 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,8 +4,16 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_METHOD, CONF_NAME, @@ -20,6 +28,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -115,16 +124,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] + self._attr_state_class = STATE_CLASS_MEASUREMENT async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() + self._attr_last_reset = dt_util.utcnow() if state: try: self._state = Decimal(state.state) - except ValueError as err: + except (DecimalException, ValueError) as err: _LOGGER.warning("Could not restore last state: %s", err) + else: + last_reset = dt_util.parse_datetime( + state.attributes.get(ATTR_LAST_RESET, "") + ) + self._attr_last_reset = ( + last_reset if last_reset else dt_util.utc_from_timestamp(0) + ) + self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) @callback def calc_integration(event): @@ -143,7 +162,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) - + if ( + self.device_class is None + and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + ): + self._attr_device_class = DEVICE_CLASS_ENERGY try: # integration as the Riemann integral of previous measures. area = 0 diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3afa5c14c22..dd6bf980d0f 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -2,12 +2,22 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TIME_SECONDS, +) +from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import mock_restore_cache -async def test_state(hass): + +async def test_state(hass) -> None: """Test integration sensor state.""" config = { "sensor": { @@ -19,15 +29,25 @@ async def test_state(hass): } } - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1, {}) - await hass.async_block_till_done() - - now = dt_util.utcnow() + timedelta(seconds=3600) + now = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.states.async_set(entity_id, 1, {}, force_update=True) + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert "device_class" not in state.attributes + + future_now = dt_util.utcnow() + timedelta(seconds=3600) + with patch("homeassistant.util.dt.utcnow", return_value=future_now): + hass.states.async_set( + entity_id, 1, {"device_class": DEVICE_CLASS_POWER}, force_update=True + ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -37,6 +57,82 @@ async def test_state(hass): assert round(float(state.state), config["sensor"]["round"]) == 1.0 assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert state.attributes.get("last_reset") == now.isoformat() + + +async def test_restore_state(hass: HomeAssistant) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache( + hass, + ( + State( + "sensor.integration", + "100.0", + { + "last_reset": "2019-10-06T21:00:00", + "device_class": DEVICE_CLASS_ENERGY, + }, + ), + ), + ) + + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "unit": ENERGY_KILO_WATT_HOUR, + "round": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == "100.00" + assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY + assert state.attributes.get("last_reset") == "2019-10-06T21:00:00" + + +async def test_restore_state_failed(hass: HomeAssistant) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache( + hass, + ( + State( + "sensor.integration", + "INVALID", + { + "last_reset": "2019-10-06T21:00:00.000000", + }, + ), + ), + ) + + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "unit": ENERGY_KILO_WATT_HOUR, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == "0" + assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert state.attributes.get("last_reset") != "2019-10-06T21:00:00" + assert "device_class" not in state.attributes async def test_trapezoidal(hass): From c6f588fc081eb864c16bb88344dcb72db4f0bbe4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 20:58:48 +0200 Subject: [PATCH 026/199] Revert "Add Automate Pulse Hub v2 support (#39501)" (#53704) --- .coveragerc | 6 - CODEOWNERS | 1 - homeassistant/components/automate/__init__.py | 36 ----- homeassistant/components/automate/base.py | 93 ----------- .../components/automate/config_flow.py | 37 ----- homeassistant/components/automate/const.py | 6 - homeassistant/components/automate/cover.py | 147 ------------------ homeassistant/components/automate/helpers.py | 46 ------ homeassistant/components/automate/hub.py | 89 ----------- .../components/automate/manifest.json | 13 -- .../components/automate/strings.json | 19 --- .../components/automate/translations/ca.json | 19 --- .../components/automate/translations/de.json | 19 --- .../components/automate/translations/en.json | 19 --- .../components/automate/translations/et.json | 19 --- .../components/automate/translations/he.json | 19 --- .../components/automate/translations/nl.json | 19 --- .../components/automate/translations/pl.json | 19 --- .../components/automate/translations/ru.json | 19 --- .../automate/translations/zh-Hant.json | 19 --- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/automate/__init__.py | 1 - tests/components/automate/test_config_flow.py | 69 -------- 25 files changed, 741 deletions(-) delete mode 100644 homeassistant/components/automate/__init__.py delete mode 100644 homeassistant/components/automate/base.py delete mode 100644 homeassistant/components/automate/config_flow.py delete mode 100644 homeassistant/components/automate/const.py delete mode 100644 homeassistant/components/automate/cover.py delete mode 100644 homeassistant/components/automate/helpers.py delete mode 100644 homeassistant/components/automate/hub.py delete mode 100644 homeassistant/components/automate/manifest.json delete mode 100644 homeassistant/components/automate/strings.json delete mode 100644 homeassistant/components/automate/translations/ca.json delete mode 100644 homeassistant/components/automate/translations/de.json delete mode 100644 homeassistant/components/automate/translations/en.json delete mode 100644 homeassistant/components/automate/translations/et.json delete mode 100644 homeassistant/components/automate/translations/he.json delete mode 100644 homeassistant/components/automate/translations/nl.json delete mode 100644 homeassistant/components/automate/translations/pl.json delete mode 100644 homeassistant/components/automate/translations/ru.json delete mode 100644 homeassistant/components/automate/translations/zh-Hant.json delete mode 100644 tests/components/automate/__init__.py delete mode 100644 tests/components/automate/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c3516739798..1f28a9a2aee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -75,12 +75,6 @@ omit = homeassistant/components/asuswrt/router.py homeassistant/components/aten_pe/* homeassistant/components/atome/* - homeassistant/components/automate/__init__.py - homeassistant/components/automate/base.py - homeassistant/components/automate/const.py - homeassistant/components/automate/cover.py - homeassistant/components/automate/helpers.py - homeassistant/components/automate/hub.py homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py diff --git a/CODEOWNERS b/CODEOWNERS index c4cb6d242d0..29906631254 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,7 +56,6 @@ homeassistant/components/august/* @bdraco homeassistant/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core -homeassistant/components/automate/* @sillyfrog homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf diff --git a/homeassistant/components/automate/__init__.py b/homeassistant/components/automate/__init__.py deleted file mode 100644 index c4f34d96a05..00000000000 --- a/homeassistant/components/automate/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The Automate Pulse Hub v2 integration.""" -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import DOMAIN -from .hub import PulseHub - -PLATFORMS = ["cover"] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Automate Pulse Hub v2 from a config entry.""" - hub = PulseHub(hass, entry) - - if not await hub.async_setup(): - return False - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = hub - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - hub = hass.data[DOMAIN][entry.entry_id] - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - if not await hub.async_reset(): - return False - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/automate/base.py b/homeassistant/components/automate/base.py deleted file mode 100644 index de37933e54d..00000000000 --- a/homeassistant/components/automate/base.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Base class for Automate Roller Blinds.""" -import logging - -import aiopulse2 - -from homeassistant.core import callback -from homeassistant.helpers import entity -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg - -from .const import AUTOMATE_ENTITY_REMOVE, DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AutomateBase(entity.Entity): - """Base representation of an Automate roller.""" - - def __init__(self, roller: aiopulse2.Roller) -> None: - """Initialize the roller.""" - self.roller = roller - - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - return self.roller.online and self.roller.hub.connected - - async def async_remove_and_unregister(self): - """Unregister from entity and device registry and call entity remove function.""" - _LOGGER.info("Removing %s %s", self.__class__.__name__, self.unique_id) - - ent_registry = await get_ent_reg(self.hass) - if self.entity_id in ent_registry.entities: - ent_registry.async_remove(self.entity_id) - - dev_registry = await get_dev_reg(self.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, self.unique_id)}, connections=set() - ) - if device is not None: - dev_registry.async_update_device( - device.id, remove_config_entry_id=self.registry_entry.config_entry_id - ) - - await self.async_remove() - - async def async_added_to_hass(self): - """Entity has been added to hass.""" - self.roller.callback_subscribe(self.notify_update) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - AUTOMATE_ENTITY_REMOVE.format(self.roller.id), - self.async_remove_and_unregister, - ) - ) - - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - self.roller.callback_unsubscribe(self.notify_update) - - @callback - def notify_update(self, roller: aiopulse2.Roller): - """Write updated device state information.""" - _LOGGER.debug( - "Device update notification received: %s (%r)", roller.id, roller.name - ) - self.async_write_ha_state() - - @property - def should_poll(self): - """Report that Automate entities do not need polling.""" - return False - - @property - def unique_id(self): - """Return the unique ID of this roller.""" - return self.roller.id - - @property - def name(self): - """Return the name of roller.""" - return self.roller.name - - @property - def device_info(self): - """Return the device info.""" - attrs = { - "identifiers": {(DOMAIN, self.roller.id)}, - } - return attrs diff --git a/homeassistant/components/automate/config_flow.py b/homeassistant/components/automate/config_flow.py deleted file mode 100644 index 45d3a5b9349..00000000000 --- a/homeassistant/components/automate/config_flow.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Config flow for Automate Pulse Hub v2 integration.""" -import logging - -import aiopulse2 -import voluptuous as vol - -from homeassistant import config_entries - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Automate Pulse Hub v2.""" - - VERSION = 1 - - async def async_step_user(self, user_input=None): - """Handle the initial step once we have info from the user.""" - if user_input is not None: - try: - hub = aiopulse2.Hub(user_input["host"]) - await hub.test() - title = hub.name - except Exception: # pylint: disable=broad-except - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "cannot_connect"}, - ) - - return self.async_create_entry(title=title, data=user_input) - - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/automate/const.py b/homeassistant/components/automate/const.py deleted file mode 100644 index 0c1dc1bd2e5..00000000000 --- a/homeassistant/components/automate/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Automate Pulse Hub v2 integration.""" - -DOMAIN = "automate" - -AUTOMATE_HUB_UPDATE = "automate_hub_update_{}" -AUTOMATE_ENTITY_REMOVE = "automate_entity_remove_{}" diff --git a/homeassistant/components/automate/cover.py b/homeassistant/components/automate/cover.py deleted file mode 100644 index 86dcda10adf..00000000000 --- a/homeassistant/components/automate/cover.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Support for Automate Roller Blinds.""" -import aiopulse2 - -from homeassistant.components.cover import ( - ATTR_POSITION, - DEVICE_CLASS_SHADE, - SUPPORT_CLOSE, - SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, - SUPPORT_OPEN_TILT, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, - SUPPORT_STOP_TILT, - CoverEntity, -) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from .base import AutomateBase -from .const import AUTOMATE_HUB_UPDATE, DOMAIN -from .helpers import async_add_automate_entities - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Automate Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] - - current = set() - - @callback - def async_add_automate_covers(): - async_add_automate_entities( - hass, AutomateCover, config_entry, current, async_add_entities - ) - - hub.cleanup_callbacks.append( - async_dispatcher_connect( - hass, - AUTOMATE_HUB_UPDATE.format(config_entry.entry_id), - async_add_automate_covers, - ) - ) - - -class AutomateCover(AutomateBase, CoverEntity): - """Representation of a Automate cover device.""" - - @property - def current_cover_position(self): - """Return the current position of the roller blind. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = None - if self.roller.closed_percent is not None: - position = 100 - self.roller.closed_percent - return position - - @property - def current_cover_tilt_position(self): - """Return the current tilt of the roller blind. - - None is unknown, 0 is closed, 100 is fully open. - """ - return None - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = 0 - if self.current_cover_position is not None: - supported_features |= ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION - ) - if self.current_cover_tilt_position is not None: - supported_features |= ( - SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - | SUPPORT_SET_TILT_POSITION - ) - - return supported_features - - @property - def device_info(self): - """Return the device info.""" - attrs = super().device_info - attrs["manufacturer"] = "Automate" - attrs["model"] = self.roller.devicetype - attrs["sw_version"] = self.roller.version - attrs["via_device"] = (DOMAIN, self.roller.hub.id) - attrs["name"] = self.name - return attrs - - @property - def device_class(self): - """Class of the cover, a shade.""" - return DEVICE_CLASS_SHADE - - @property - def is_opening(self): - """Is cover opening/moving up.""" - return self.roller.action == aiopulse2.MovingAction.up - - @property - def is_closing(self): - """Is cover closing/moving down.""" - return self.roller.action == aiopulse2.MovingAction.down - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self.roller.closed_percent == 100 - - async def async_close_cover(self, **kwargs): - """Close the roller.""" - await self.roller.move_down() - - async def async_open_cover(self, **kwargs): - """Open the roller.""" - await self.roller.move_up() - - async def async_stop_cover(self, **kwargs): - """Stop the roller.""" - await self.roller.move_stop() - - async def async_set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - await self.roller.move_to(100 - kwargs[ATTR_POSITION]) - - async def async_close_cover_tilt(self, **kwargs): - """Close the roller.""" - await self.roller.move_down() - - async def async_open_cover_tilt(self, **kwargs): - """Open the roller.""" - await self.roller.move_up() - - async def async_stop_cover_tilt(self, **kwargs): - """Stop the roller.""" - await self.roller.move_stop() - - async def async_set_cover_tilt(self, **kwargs): - """Tilt the roller shutter to a specific position.""" - await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/automate/helpers.py b/homeassistant/components/automate/helpers.py deleted file mode 100644 index 92130eeb79b..00000000000 --- a/homeassistant/components/automate/helpers.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Helper functions for Automate Pulse.""" -import logging - -from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -@callback -def async_add_automate_entities( - hass, entity_class, config_entry, current, async_add_entities -): - """Add any new entities.""" - hub = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) - - api = hub.api.rollers - - new_items = [] - for unique_id, roller in api.items(): - if unique_id not in current: - _LOGGER.debug("New %s %s", entity_class.__name__, unique_id) - new_item = entity_class(roller) - current.add(unique_id) - new_items.append(new_item) - - async_add_entities(new_items) - - -async def update_devices(hass, config_entry, api): - """Tell hass that device info has been updated.""" - dev_registry = await get_dev_reg(hass) - - for api_item in api.values(): - # Update Device name - device = dev_registry.async_get_device( - identifiers={(DOMAIN, api_item.id)}, connections=set() - ) - if device is not None: - dev_registry.async_update_device( - device.id, - name=api_item.name, - ) diff --git a/homeassistant/components/automate/hub.py b/homeassistant/components/automate/hub.py deleted file mode 100644 index 78e1b5873fa..00000000000 --- a/homeassistant/components/automate/hub.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Code to handle a Pulse Hub.""" -from __future__ import annotations - -import asyncio -import logging - -import aiopulse2 - -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from .const import AUTOMATE_ENTITY_REMOVE, AUTOMATE_HUB_UPDATE -from .helpers import update_devices - -_LOGGER = logging.getLogger(__name__) - - -class PulseHub: - """Manages a single Pulse Hub.""" - - def __init__(self, hass, config_entry): - """Initialize the system.""" - self.config_entry = config_entry - self.hass = hass - self.api: aiopulse2.Hub | None = None - self.tasks = [] - self.current_rollers = {} - self.cleanup_callbacks = [] - - @property - def title(self): - """Return the title of the hub shown in the integrations list.""" - return f"{self.api.name} ({self.api.host})" - - @property - def host(self): - """Return the host of this hub.""" - return self.config_entry.data["host"] - - async def async_setup(self): - """Set up a hub based on host parameter.""" - host = self.host - - hub = aiopulse2.Hub(host, propagate_callbacks=True) - - self.api = hub - - hub.callback_subscribe(self.async_notify_update) - self.tasks.append(asyncio.create_task(hub.run())) - - _LOGGER.debug("Hub setup complete") - return True - - async def async_reset(self): - """Reset this hub to default state.""" - for cleanup_callback in self.cleanup_callbacks: - cleanup_callback() - - # If not setup - if self.api is None: - return False - - self.api.callback_unsubscribe(self.async_notify_update) - await self.api.stop() - del self.api - self.api = None - - # Wait for any running tasks to complete - await asyncio.wait(self.tasks) - - return True - - async def async_notify_update(self, hub=None): - """Evaluate entities when hub reports that update has occurred.""" - _LOGGER.debug("Hub {self.title} updated") - - await update_devices(self.hass, self.config_entry, self.api.rollers) - self.hass.config_entries.async_update_entry(self.config_entry, title=self.title) - - async_dispatcher_send( - self.hass, AUTOMATE_HUB_UPDATE.format(self.config_entry.entry_id) - ) - - for unique_id in list(self.current_rollers): - if unique_id not in self.api.rollers: - _LOGGER.debug("Notifying remove of %s", unique_id) - self.current_rollers.pop(unique_id) - async_dispatcher_send( - self.hass, AUTOMATE_ENTITY_REMOVE.format(unique_id) - ) diff --git a/homeassistant/components/automate/manifest.json b/homeassistant/components/automate/manifest.json deleted file mode 100644 index 071aaf1589f..00000000000 --- a/homeassistant/components/automate/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "automate", - "name": "Automate Pulse Hub v2", - "config_flow": true, - "iot_class": "local_push", - "documentation": "https://www.home-assistant.io/integrations/automate", - "requirements": [ - "aiopulse2==0.6.0" - ], - "codeowners": [ - "@sillyfrog" - ] -} \ No newline at end of file diff --git a/homeassistant/components/automate/strings.json b/homeassistant/components/automate/strings.json deleted file mode 100644 index 8a8131f0f67..00000000000 --- a/homeassistant/components/automate/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::host%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ca.json b/homeassistant/components/automate/translations/ca.json deleted file mode 100644 index 69cd7de20aa..00000000000 --- a/homeassistant/components/automate/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "host": "Amfitri\u00f3" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/de.json b/homeassistant/components/automate/translations/de.json deleted file mode 100644 index fa773dbf708..00000000000 --- a/homeassistant/components/automate/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/en.json b/homeassistant/components/automate/translations/en.json deleted file mode 100644 index 2ad35962b25..00000000000 --- a/homeassistant/components/automate/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/et.json b/homeassistant/components/automate/translations/et.json deleted file mode 100644 index da2c8eef4d2..00000000000 --- a/homeassistant/components/automate/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" - }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus", - "unknown": "Ootamatu t\u00f5rge" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/he.json b/homeassistant/components/automate/translations/he.json deleted file mode 100644 index accc8a0a610..00000000000 --- a/homeassistant/components/automate/translations/he.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/nl.json b/homeassistant/components/automate/translations/nl.json deleted file mode 100644 index bb92bf9e593..00000000000 --- a/homeassistant/components/automate/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "host": "Host" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/pl.json b/homeassistant/components/automate/translations/pl.json deleted file mode 100644 index 647c0921e3a..00000000000 --- a/homeassistant/components/automate/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "host": "Nazwa hosta lub adres IP" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/ru.json b/homeassistant/components/automate/translations/ru.json deleted file mode 100644 index 1212d054ff7..00000000000 --- a/homeassistant/components/automate/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/automate/translations/zh-Hant.json b/homeassistant/components/automate/translations/zh-Hant.json deleted file mode 100644 index 0fbaa19828f..00000000000 --- a/homeassistant/components/automate/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 89b1a9ba8ae..4cb9e2e3c4b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,7 +28,6 @@ FLOWS = [ "atag", "august", "aurora", - "automate", "awair", "axis", "azure_devops", diff --git a/requirements_all.txt b/requirements_all.txt index e9a46b9e079..96d5c1c3360 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,9 +220,6 @@ aionotify==0.2.0 # homeassistant.components.notion aionotion==3.0.2 -# homeassistant.components.automate -aiopulse2==0.6.0 - # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29938321ee3..a5f527a05e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,9 +142,6 @@ aiomusiccast==0.8.2 # homeassistant.components.notion aionotion==3.0.2 -# homeassistant.components.automate -aiopulse2==0.6.0 - # homeassistant.components.acmeda aiopulse==0.4.2 diff --git a/tests/components/automate/__init__.py b/tests/components/automate/__init__.py deleted file mode 100644 index 6a87ba942e3..00000000000 --- a/tests/components/automate/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Automate Pulse Hub v2 integration.""" diff --git a/tests/components/automate/test_config_flow.py b/tests/components/automate/test_config_flow.py deleted file mode 100644 index fea2fa995cd..00000000000 --- a/tests/components/automate/test_config_flow.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Test the Automate Pulse Hub v2 config flow.""" -from unittest.mock import Mock, patch - -from homeassistant import config_entries, setup -from homeassistant.components.automate.const import DOMAIN - - -def mock_hub(testfunc=None): - """Mock aiopulse2.Hub.""" - Hub = Mock() - Hub.name = "Name of the device" - - async def hub_test(): - if testfunc: - testfunc() - - Hub.test = hub_test - - return Hub - - -async def test_form(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] is None - - with patch("aiopulse2.Hub", return_value=mock_hub()), patch( - "homeassistant.components.automate.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - }, - ) - - assert result2["type"] == "create_entry" - assert result2["title"] == "Name of the device" - assert result2["data"] == { - "host": "1.1.1.1", - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - def raise_error(): - raise ConnectionRefusedError - - with patch("aiopulse2.Hub", return_value=mock_hub(raise_error)): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": "1.1.1.1", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} From b2187022c49d84f1db4a2302458f124575a12d4a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 Jul 2021 20:54:51 +0200 Subject: [PATCH 027/199] Set state class measurement also for Total Energy for AVM Fritz!Smarthome devices (#53707) --- homeassistant/components/fritzbox/sensor.py | 2 +- tests/components/fritzbox/test_switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 24b3d9cc5ff..d325e592faf 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}_total_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: None, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, coordinator, ain, diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index cb6ae85f889..951528f1e7d 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -76,7 +76,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_LAST_RESET] == "1970-01-01T00:00:00+00:00" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert ATTR_STATE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT async def test_turn_on(hass: HomeAssistant, fritz: Mock): From 4b2a1ec694998195b16946f54498b8e42ee32b6f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 29 Jul 2021 21:10:53 +0200 Subject: [PATCH 028/199] Add last reset to Shelly's energy entities (#53710) --- homeassistant/components/shelly/const.py | 3 +++ homeassistant/components/shelly/entity.py | 4 ++-- homeassistant/components/shelly/sensor.py | 21 +++++++++++++++++---- homeassistant/components/shelly/utils.py | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 2c401829c30..49e33dfd5e1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -92,3 +92,6 @@ KELVIN_MIN_VALUE_WHITE: Final = 2700 KELVIN_MIN_VALUE_COLOR: Final = 3000 UPTIME_DEVIATION: Final = 5 + +LAST_RESET_UPTIME: Final = "uptime" +LAST_RESET_NEVER: Final = "never" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index c8b23f71bd7..a1ce2e671d1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from datetime import datetime import logging from typing import Any, Callable, Final, cast @@ -180,7 +179,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None - last_reset: datetime | None = None + last_reset: str | None = None @dataclass @@ -286,6 +285,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit: None | str | Callable[[dict], str] = unit self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) + self._last_value: str | None = None @property def unique_id(self) -> str: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7c2ffdbc470..56e4f63bc75 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt -from .const import SHAIR_MAX_WORK_HOURS +from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -114,6 +114,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("emeter", "energy"): BlockAttributeDescription( name="Energy", @@ -121,7 +122,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + last_reset=LAST_RESET_NEVER, ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", @@ -129,7 +130,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + last_reset=LAST_RESET_NEVER, ), ("light", "energy"): BlockAttributeDescription( name="Energy", @@ -138,6 +139,7 @@ SENSORS: Final = { device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + last_reset=LAST_RESET_UPTIME, ), ("relay", "energy"): BlockAttributeDescription( name="Energy", @@ -145,6 +147,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("roller", "rollerEnergy"): BlockAttributeDescription( name="Energy", @@ -152,6 +155,7 @@ SENSORS: Final = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_MEASUREMENT, + last_reset=LAST_RESET_UPTIME, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", @@ -264,7 +268,16 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): @property def last_reset(self) -> datetime | None: """State class of sensor.""" - return self.description.last_reset + if self.description.last_reset == LAST_RESET_UPTIME: + self._last_value = get_device_uptime( + self.wrapper.device.status, self._last_value + ) + return dt.parse_datetime(self._last_value) + + if self.description.last_reset == LAST_RESET_NEVER: + return dt.utc_from_timestamp(0) + + return None @property def unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d8ce5ae9e45..d1e2947d5ac 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -136,7 +136,7 @@ def is_momentary_input(settings: dict[str, Any], block: aioshelly.Block) -> bool return button_type in ["momentary", "momentary_on_release"] -def get_device_uptime(status: dict[str, Any], last_uptime: str) -> str: +def get_device_uptime(status: dict[str, Any], last_uptime: str | None) -> str: """Return device uptime string, tolerate up to 5 seconds deviation.""" delta_uptime = utcnow() - timedelta(seconds=status["uptime"]) From dc2494f0a0318d2a643b929308450b021145c8bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 21:25:18 +0200 Subject: [PATCH 029/199] Add state class support to DSMR Reader (#53715) --- .../components/dsmr_reader/definitions.py | 998 ++++++++++-------- .../components/dsmr_reader/sensor.py | 74 +- 2 files changed, 553 insertions(+), 519 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 51aaca24c02..1a46f86132b 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -1,5 +1,13 @@ """Definitions for DSMR Reader sensors added to MQTT.""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Callable + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, @@ -13,6 +21,7 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util def dsmr_transform(value): @@ -29,462 +38,533 @@ def tariff_transform(value): return "high" -DEFINITIONS = { - "dsmr/reading/electricity_delivered_1": { - "name": "Low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_returned_1": { - "name": "Low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_delivered_2": { - "name": "High tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_returned_2": { - "name": "High tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/reading/electricity_currently_delivered": { - "name": "Current power usage", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/electricity_currently_returned": { - "name": "Current power return", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l1": { - "name": "Current power usage L1", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l2": { - "name": "Current power usage L2", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_delivered_l3": { - "name": "Current power usage L3", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l1": { - "name": "Current power return L1", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l2": { - "name": "Current power return L2", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/phase_currently_returned_l3": { - "name": "Current power return L3", - "enable_default": True, - "device_class": DEVICE_CLASS_POWER, - "unit": POWER_KILO_WATT, - }, - "dsmr/reading/extra_device_delivered": { - "name": "Gas meter usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/reading/phase_voltage_l1": { - "name": "Current voltage L1", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_voltage_l2": { - "name": "Current voltage L2", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_voltage_l3": { - "name": "Current voltage L3", - "enable_default": True, - "device_class": DEVICE_CLASS_VOLTAGE, - "unit": ELECTRIC_POTENTIAL_VOLT, - }, - "dsmr/reading/phase_power_current_l1": { - "name": "Phase power current L1", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/phase_power_current_l2": { - "name": "Phase power current L2", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/phase_power_current_l3": { - "name": "Phase power current L3", - "enable_default": True, - "device_class": DEVICE_CLASS_CURRENT, - "unit": ELECTRIC_CURRENT_AMPERE, - }, - "dsmr/reading/timestamp": { - "name": "Telegram timestamp", - "enable_default": False, - "device_class": DEVICE_CLASS_TIMESTAMP, - }, - "dsmr/consumption/gas/delivered": { - "name": "Gas usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/consumption/gas/currently_delivered": { - "name": "Current gas usage", - "enable_default": True, - "icon": "mdi:fire", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/consumption/gas/read_at": { - "name": "Gas meter read", - "enable_default": True, - "device_class": DEVICE_CLASS_TIMESTAMP, - }, - "dsmr/day-consumption/electricity1": { - "name": "Low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity2": { - "name": "High tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity1_returned": { - "name": "Low tariff return", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity2_returned": { - "name": "High tariff return", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity_merged": { - "name": "Power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity_returned_merged": { - "name": "Power return total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/day-consumption/electricity1_cost": { - "name": "Low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/electricity2_cost": { - "name": "High tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/electricity_cost_merged": { - "name": "Power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/gas": { - "name": "Gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/day-consumption/gas_cost": { - "name": "Gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/total_cost": { - "name": "Total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { - "name": "Low tariff delivered price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { - "name": "High tariff delivered price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { - "name": "Low tariff returned price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { - "name": "High tariff returned price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/energy_supplier_price_gas": { - "name": "Gas price", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/day-consumption/fixed_cost": { - "name": "Current day fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/meter-stats/dsmr_version": { - "name": "DSMR version", - "enable_default": True, - "icon": "mdi:alert-circle", - "transform": dsmr_transform, - }, - "dsmr/meter-stats/electricity_tariff": { - "name": "Electricity tariff", - "enable_default": True, - "icon": "mdi:flash", - "transform": tariff_transform, - }, - "dsmr/meter-stats/power_failure_count": { - "name": "Power failure count", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/long_power_failure_count": { - "name": "Long power failure count", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l1": { - "name": "Voltage sag L1", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l2": { - "name": "Voltage sag L2", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_sag_count_l3": { - "name": "Voltage sag L3", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l1": { - "name": "Voltage swell L1", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l2": { - "name": "Voltage swell L2", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/voltage_swell_count_l3": { - "name": "Voltage swell L3", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/meter-stats/rejected_telegrams": { - "name": "Rejected telegrams", - "enable_default": True, - "icon": "mdi:flash", - }, - "dsmr/current-month/electricity1": { - "name": "Current month low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity2": { - "name": "Current month high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity1_returned": { - "name": "Current month low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity2_returned": { - "name": "Current month high tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity_merged": { - "name": "Current month power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity_returned_merged": { - "name": "Current month power return total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-month/electricity1_cost": { - "name": "Current month low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/electricity2_cost": { - "name": "Current month high tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/electricity_cost_merged": { - "name": "Current month power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/gas": { - "name": "Current month gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/current-month/gas_cost": { - "name": "Current month gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/fixed_cost": { - "name": "Current month fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-month/total_cost": { - "name": "Current month total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity1": { - "name": "Current year low tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity2": { - "name": "Current year high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity1_returned": { - "name": "Current year low tariff returned", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity2_returned": { - "name": "Current year high tariff usage", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity_merged": { - "name": "Current year power usage total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity_returned_merged": { - "name": "Current year power returned total", - "enable_default": True, - "device_class": DEVICE_CLASS_ENERGY, - "unit": ENERGY_KILO_WATT_HOUR, - }, - "dsmr/current-year/electricity1_cost": { - "name": "Current year low tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity2_cost": { - "name": "Current year high tariff cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/electricity_cost_merged": { - "name": "Current year power total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/gas": { - "name": "Current year gas usage", - "enable_default": True, - "icon": "mdi:counter", - "unit": VOLUME_CUBIC_METERS, - }, - "dsmr/current-year/gas_cost": { - "name": "Current year gas cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/fixed_cost": { - "name": "Current year fixed cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, - "dsmr/current-year/total_cost": { - "name": "Current year total cost", - "enable_default": True, - "icon": "mdi:currency-eur", - "unit": CURRENCY_EURO, - }, -} +@dataclass +class DSMRReaderSensorEntityDescription(SensorEntityDescription): + """Sensor entity description for DSMR Reader.""" + + state: Callable | None = None + + +SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_delivered_1", + name="Low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_returned_1", + name="Low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_delivered_2", + name="High tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_returned_2", + name="High tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_currently_delivered", + name="Current power usage", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/electricity_currently_returned", + name="Current power return", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l1", + name="Current power usage L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l2", + name="Current power usage L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_delivered_l3", + name="Current power usage L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l1", + name="Current power return L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l2", + name="Current power return L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_currently_returned_l3", + name="Current power return L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/extra_device_delivered", + name="Gas meter usage", + entity_registry_enabled_default=False, + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l1", + name="Current voltage L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l2", + name="Current voltage L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_voltage_l3", + name="Current voltage L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l1", + name="Phase power current L1", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l2", + name="Phase power current L2", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/phase_power_current_l3", + name="Phase power current L3", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/reading/timestamp", + name="Telegram timestamp", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/delivered", + name="Gas usage", + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/currently_delivered", + name="Current gas usage", + icon="mdi:fire", + unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/consumption/gas/read_at", + name="Gas meter read", + entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1", + name="Low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2", + name="High tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1_returned", + name="Low tariff return", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2_returned", + name="High tariff return", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_merged", + name="Power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_returned_merged", + name="Power return total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt_util.utc_from_timestamp(0), + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity1_cost", + name="Low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity2_cost", + name="High tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/electricity_cost_merged", + name="Power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/gas", + name="Gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/gas_cost", + name="Gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/total_cost", + name="Total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_1", + name="Low tariff delivered price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_2", + name="High tariff delivered price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_returned_1", + name="Low tariff returned price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_electricity_returned_2", + name="High tariff returned price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/energy_supplier_price_gas", + name="Gas price", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/day-consumption/fixed_cost", + name="Current day fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/dsmr_version", + name="DSMR version", + entity_registry_enabled_default=False, + icon="mdi:alert-circle", + state=dsmr_transform, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/electricity_tariff", + name="Electricity tariff", + icon="mdi:flash", + state=tariff_transform, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/power_failure_count", + name="Power failure count", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/long_power_failure_count", + name="Long power failure count", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l1", + name="Voltage sag L1", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l2", + name="Voltage sag L2", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_sag_count_l3", + name="Voltage sag L3", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l1", + name="Voltage swell L1", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l2", + name="Voltage swell L2", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/voltage_swell_count_l3", + name="Voltage swell L3", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/meter-stats/rejected_telegrams", + name="Rejected telegrams", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1", + name="Current month low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2", + name="Current month high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1_returned", + name="Current month low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2_returned", + name="Current month high tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_merged", + name="Current month power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_returned_merged", + name="Current month power return total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity1_cost", + name="Current month low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity2_cost", + name="Current month high tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/electricity_cost_merged", + name="Current month power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/gas", + name="Current month gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/gas_cost", + name="Current month gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/fixed_cost", + name="Current month fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-month/total_cost", + name="Current month total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1", + name="Current year low tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2", + name="Current year high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1_returned", + name="Current year low tariff returned", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2_returned", + name="Current year high tariff usage", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_merged", + name="Current year power usage total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_returned_merged", + name="Current year power returned total", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity1_cost", + name="Current year low tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity2_cost", + name="Current year high tariff cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/electricity_cost_merged", + name="Current year power total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/gas", + name="Current year gas usage", + icon="mdi:counter", + unit_of_measurement=VOLUME_CUBIC_METERS, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/gas_cost", + name="Current year gas cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/fixed_cost", + name="Current year fixed cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), + DSMRReaderSensorEntityDescription( + key="dsmr/current-year/total_cost", + name="Current year total cost", + icon="mdi:currency-eur", + unit_of_measurement=CURRENCY_EURO, + ), +) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 0ee5932c1bb..39356db46b5 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -4,39 +4,27 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.util import slugify -from .definitions import DEFINITIONS +from .definitions import SENSORS, DSMRReaderSensorEntityDescription DOMAIN = "dsmr_reader" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up DSMR Reader sensors.""" - - sensors = [] - for topic in DEFINITIONS: - sensors.append(DSMRSensor(topic)) - - async_add_entities(sensors) + async_add_entities(DSMRSensor(description) for description in SENSORS) class DSMRSensor(SensorEntity): """Representation of a DSMR sensor that is updated via MQTT.""" - def __init__(self, topic): + entity_description: DSMRReaderSensorEntityDescription + + def __init__(self, description: DSMRReaderSensorEntityDescription) -> None: """Initialize the sensor.""" + self.entity_description = description - self._definition = DEFINITIONS[topic] - - self._entity_id = slugify(topic.replace("/", "_")) - self._topic = topic - - self._name = self._definition.get("name", topic.split("/")[-1]) - self._device_class = self._definition.get("device_class") - self._enable_default = self._definition.get("enable_default") - self._unit_of_measurement = self._definition.get("unit") - self._icon = self._definition.get("icon") - self._transform = self._definition.get("transform") - self._state = None + slug = slugify(description.key.replace("/", "_")) + self.entity_id = f"sensor.{slug}" async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -44,47 +32,13 @@ class DSMRSensor(SensorEntity): @callback def message_received(message): """Handle new MQTT messages.""" - - if self._transform is not None: - self._state = self._transform(message.payload) + if self.entity_description.state is not None: + self._attr_state = self.entity_description.state(message.payload) else: - self._state = message.payload + self._attr_state = message.payload self.async_write_ha_state() - await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) - - @property - def name(self): - """Return the name of the sensor supplied in constructor.""" - return self._name - - @property - def entity_id(self): - """Return the entity ID for this sensor.""" - return f"sensor.{self._entity_id}" - - @property - def state(self): - """Return the current state of the entity.""" - return self._state - - @property - def device_class(self): - """Return the device_class of this sensor.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of this sensor.""" - return self._unit_of_measurement - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enable_default - - @property - def icon(self): - """Return the icon of this sensor.""" - return self._icon + await mqtt.async_subscribe( + self.hass, self.entity_description.key, message_received, 1 + ) From cc0aa32f3e2fa77082794f5cd917220980a8babf Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 21:25:43 +0200 Subject: [PATCH 030/199] Fix zwave_js meter sensor state class (#53716) --- homeassistant/components/zwave_js/sensor.py | 3 +-- tests/components/zwave_js/test_sensor.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 39aa2f30604..f4b303aa1af 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -229,8 +229,6 @@ class ZWaveNumericSensor(ZwaveSensorBase): class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): """Representation of a Z-Wave Meter CC sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT - def __init__( self, config_entry: ConfigEntry, @@ -241,6 +239,7 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): super().__init__(config_entry, client, info) # Entity class attributes + self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_last_reset = dt.utc_from_timestamp(0) self._attr_device_class = DEVICE_CLASS_POWER if self.info.primary_value.metadata.unit == "kWh": diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 29fed2b5c55..ebe406fb951 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch from zwave_js_server.event import Event -from homeassistant.components.sensor import ATTR_LAST_RESET +from homeassistant.components.sensor import ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -62,6 +62,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.0" assert state.attributes["unit_of_measurement"] == POWER_WATT assert state.attributes["device_class"] == DEVICE_CLASS_POWER + assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT state = hass.states.get(ENERGY_SENSOR) @@ -69,6 +70,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.16" assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY + assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT async def test_disabled_notification_sensor(hass, multisensor_6, integration): From b1758e1fcc351df2603d730f84bf8ee047b54715 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 12:34:32 -0700 Subject: [PATCH 031/199] Bump frontend to 20210729.0 (#53717) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cf835396fba..ac791977038 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210728.0" + "home-assistant-frontend==20210729.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0556c6e5452..e1acf1dba15 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 96d5c1c3360..b7f8ec34eda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5f527a05e6..e8b1a136048 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210728.0 +home-assistant-frontend==20210729.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bf6133534dfe27e9557fb8482b11592156011c31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 21:34:22 +0200 Subject: [PATCH 032/199] Fix SolarEdge statistics; missing device_class (#53720) --- homeassistant/components/solaredge/const.py | 57 ++++++++++++-------- homeassistant/components/solaredge/models.py | 16 ++---- homeassistant/components/solaredge/sensor.py | 45 ++++++++-------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 81d9bc5aebe..872781bf19c 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -3,10 +3,16 @@ from datetime import timedelta import logging from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) from homeassistant.util import dt as dt_util -from .models import SolarEdgeSensor +from .models import SolarEdgeSensorEntityDescription DOMAIN = "solaredge" @@ -29,7 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=15) # Supported overview sensors SENSOR_TYPES = [ - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="lifetime_energy", json_key="lifeTimeData", name="Lifetime energy", @@ -37,138 +43,143 @@ SENSOR_TYPES = [ last_reset=dt_util.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_this_year", json_key="lastYearData", name="Energy this year", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_this_month", json_key="lastMonthData", name="Energy this month", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="energy_today", json_key="lastDayData", name="Energy today", entity_registry_enabled_default=False, icon="mdi:solar-power", unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="current_power", json_key="currentPower", name="Current Power", icon="mdi:solar-power", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="site_details", name="Site details", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="meters", json_key="meters", name="Meters", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="sensors", json_key="sensors", name="Sensors", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="gateways", json_key="gateways", name="Gateways", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="batteries", json_key="batteries", name="Batteries", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="inverters", json_key="inverters", name="Inverters", entity_registry_enabled_default=False, ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="power_consumption", json_key="LOAD", name="Power Consumption", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="solar_power", json_key="PV", name="Solar Power", entity_registry_enabled_default=False, icon="mdi:solar-power", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="grid_power", json_key="GRID", name="Grid Power", entity_registry_enabled_default=False, icon="mdi:power-plug", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="storage_power", json_key="STORAGE", name="Storage Power", entity_registry_enabled_default=False, icon="mdi:car-battery", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="purchased_power", json_key="Purchased", name="Imported Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="production_power", json_key="Production", name="Production Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="consumption_power", json_key="Consumption", name="Consumption Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="selfconsumption_power", json_key="SelfConsumption", name="SelfConsumption Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="feedin_power", json_key="FeedIn", name="Exported Power", entity_registry_enabled_default=False, icon="mdi:flash", ), - SolarEdgeSensor( + SolarEdgeSensorEntityDescription( key="storage_level", json_key="STORAGE", name="Storage Level", diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py index f91db9ee9ff..ce24d854aac 100644 --- a/homeassistant/components/solaredge/models.py +++ b/homeassistant/components/solaredge/models.py @@ -2,20 +2,12 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime + +from homeassistant.components.sensor import SensorEntityDescription @dataclass -class SolarEdgeSensor: - """Represents an SolarEdge Sensor.""" - - key: str - name: str +class SolarEdgeSensorEntityDescription(SensorEntityDescription): + """Sensor entity description for SolarEdge.""" json_key: str | None = None - device_class: str | None = None - entity_registry_enabled_default: bool = True - icon: str | None = None - last_reset: datetime | None = None - state_class: str | None = None - unit_of_measurement: str | None = None diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 23cfa116599..85e01a2d7ee 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -21,7 +21,7 @@ from .coordinator import ( SolarEdgeOverviewDataService, SolarEdgePowerFlowDataService, ) -from .models import SolarEdgeSensor +from .models import SolarEdgeSensorEntityDescription async def async_setup_entry( @@ -68,7 +68,8 @@ class SolarEdgeSensorFactory: self.services: dict[ str, tuple[ - type[SolarEdgeSensor | SolarEdgeOverviewSensor], SolarEdgeDataService + type[SolarEdgeSensorEntity | SolarEdgeOverviewSensor], + SolarEdgeDataService, ], ] = {"site_details": (SolarEdgeDetailsSensor, details)} @@ -99,7 +100,9 @@ class SolarEdgeSensorFactory: ): self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) - def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor: + def create_sensor( + self, sensor_type: SolarEdgeSensorEntityDescription + ) -> SolarEdgeSensorEntityDescription: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_type.key] @@ -109,27 +112,21 @@ class SolarEdgeSensorFactory: class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity): """Abstract class for a solaredge sensor.""" + entity_description: SolarEdgeSensorEntityDescription + def __init__( self, platform_name: str, - sensor_type: SolarEdgeSensor, + description: SolarEdgeSensorEntityDescription, data_service: SolarEdgeDataService, ) -> None: """Initialize the sensor.""" super().__init__(data_service.coordinator) self.platform_name = platform_name - self.sensor_type = sensor_type + self.entity_description = description self.data_service = data_service - self._attr_device_class = sensor_type.device_class - self._attr_entity_registry_enabled_default = ( - sensor_type.entity_registry_enabled_default - ) - self._attr_icon = sensor_type.icon - self._attr_last_reset = sensor_type.last_reset - self._attr_name = f"{platform_name} ({sensor_type.name})" - self._attr_state_class = sensor_type.state_class - self._attr_unit_of_measurement = sensor_type.unit_of_measurement + self._attr_name = f"{platform_name} ({description.name})" class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): @@ -138,7 +135,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): @@ -161,12 +158,12 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): @@ -181,12 +178,12 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): @@ -197,23 +194,23 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): def __init__( self, platform_name: str, - sensor_type: SolarEdgeSensor, + description: SolarEdgeSensorEntityDescription, data_service: SolarEdgeDataService, ) -> None: """Initialize the power flow sensor.""" - super().__init__(platform_name, sensor_type, data_service) + super().__init__(platform_name, description, data_service) self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self.sensor_type.json_key) + return self.data_service.attributes.get(self.entity_description.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self.sensor_type.json_key) + return self.data_service.data.get(self.entity_description.json_key) class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): @@ -224,7 +221,7 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): @property def state(self) -> str | None: """Return the state of the sensor.""" - attr = self.data_service.attributes.get(self.sensor_type.json_key) + attr = self.data_service.attributes.get(self.entity_description.json_key) if attr and "soc" in attr: return attr["soc"] return None From 6dc00d3d87afdfdd13c33a5ff76a2ea44c46e272 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 12:35:50 -0700 Subject: [PATCH 033/199] Bumped version to 2021.8.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 15d0a3485cd..c7778e23c67 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f1400b03bbada0cda3b96690e79fa255eea18053 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 22:55:26 +0200 Subject: [PATCH 034/199] Fix DSMR reconnecting loop without timeout (#53722) --- homeassistant/components/dsmr/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d5674b34520..5afc229a727 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -165,6 +165,9 @@ async def async_setup_entry( LOGGER.exception("Error connecting to DSMR") transport = None protocol = None + + # throttle reconnect attempts + await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) except CancelledError: if stop_listener: stop_listener() # pylint: disable=not-callable From 8cf0182f2fd0f06b07e7afc293a1040c467588da Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 29 Jul 2021 23:26:57 +0200 Subject: [PATCH 035/199] Fix zwave_js current and voltage meter sensor device class (#53723) --- homeassistant/components/zwave_js/sensor.py | 11 ++++++++--- tests/components/zwave_js/common.py | 4 +++- tests/components/zwave_js/test_sensor.py | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index f4b303aa1af..304a80f7940 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -22,8 +22,10 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -142,8 +144,14 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return DEVICE_CLASS_HUMIDITY if "temperature" in property_lower: return DEVICE_CLASS_TEMPERATURE + if self.info.primary_value.metadata.unit == "A": + return DEVICE_CLASS_CURRENT if self.info.primary_value.metadata.unit == "W": return DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "kWh": + return DEVICE_CLASS_ENERGY + if self.info.primary_value.metadata.unit == "V": + return DEVICE_CLASS_VOLTAGE if self.info.primary_value.metadata.unit == "Lux": return DEVICE_CLASS_ILLUMINANCE return None @@ -241,9 +249,6 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): # Entity class attributes self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_last_reset = dt.utc_from_timestamp(0) - self._attr_device_class = DEVICE_CLASS_POWER - if self.info.primary_value.metadata.unit == "kWh": - self._attr_device_class = DEVICE_CLASS_ENERGY @callback def async_update_last_reset( diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c7100b22bd5..8c8a3f2e576 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -3,8 +3,10 @@ from datetime import datetime, timezone AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" -ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" +ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" +VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3" +CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4" SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index ebe406fb951..9fa4152ad6b 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -12,10 +12,14 @@ from homeassistant.components.zwave_js.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, @@ -25,6 +29,7 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, BASIC_SENSOR, + CURRENT_SENSOR, DATETIME_LAST_RESET, DATETIME_ZERO, ENERGY_SENSOR, @@ -34,6 +39,7 @@ from .common import ( METER_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, + VOLTAGE_SENSOR, ) @@ -72,6 +78,20 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT + state = hass.states.get(VOLTAGE_SENSOR) + + assert state + assert state.state == "122.96" + assert state.attributes["unit_of_measurement"] == ELECTRIC_POTENTIAL_VOLT + assert state.attributes["device_class"] == DEVICE_CLASS_VOLTAGE + + state = hass.states.get(CURRENT_SENSOR) + + assert state + assert state.state == "0.0" + assert state.attributes["unit_of_measurement"] == ELECTRIC_CURRENT_AMPERE + assert state.attributes["device_class"] == DEVICE_CLASS_CURRENT + async def test_disabled_notification_sensor(hass, multisensor_6, integration): """Test sensor is created from Notification CC and is disabled.""" From a671a0ccacce9991bf2bc5df8aad154105bcf7a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jul 2021 23:26:39 +0200 Subject: [PATCH 036/199] Fix effect selector of light.turn_on service (#53726) --- homeassistant/components/light/services.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 778203a1c93..3b7df4e70c5 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -296,11 +296,7 @@ turn_on: name: Effect description: Light effect. selector: - select: - options: - - colorloop - - random - - white + text: turn_off: name: Turn off From 54eeebfd20d9f2ecfc6c381f4f81433caffe7942 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 14:26:05 -0700 Subject: [PATCH 037/199] Revert "Allow uploading large snapshots (#53528)" (#53729) This reverts commit cdce14d63db209acedb9888e726b813a069bf720. --- homeassistant/components/hassio/http.py | 54 ++++++++++++++++++------- tests/components/hassio/test_http.py | 49 ++++++---------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 73e5549be9a..302cc00bb9f 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,15 +1,16 @@ """HTTP Support for Hass.io.""" from __future__ import annotations +import asyncio import logging import os import re import aiohttp from aiohttp import web -from aiohttp.client import ClientError, ClientTimeout -from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway +import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded @@ -19,6 +20,8 @@ from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) +MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 + NO_TIMEOUT = re.compile( r"^(?:" r"|homeassistant/update" @@ -72,28 +75,48 @@ class HassIOView(HomeAssistantView): async def _command_proxy( self, path: str, request: web.Request - ) -> web.StreamResponse: + ) -> web.Response | web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. """ + read_timeout = _get_timeout(path) + client_timeout = 10 + data = None headers = _init_header(request) if path in ("snapshots/new/upload", "backups/new/upload"): # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access + + # Backups are big, so we need to adjust the allowed size + request._client_max_size = ( # pylint: disable=protected-access + MAX_UPLOAD_SIZE + ) + client_timeout = 300 + try: - # Stream the request to the supervisor - client = await self._websession.request( - method=request.method, - url=f"http://{self._host}/{path}", + with async_timeout.timeout(client_timeout): + data = await request.read() + + method = getattr(self._websession, request.method.lower()) + client = await method( + f"http://{self._host}/{path}", + data=data, headers=headers, - data=request.content, - timeout=_get_timeout(path), + timeout=read_timeout, ) - # Stream the supervisor response back + # Simple request + if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await client.read() + return web.Response( + content_type=client.content_type, status=client.status, body=body + ) + + # Stream response response = web.StreamResponse(status=client.status, headers=client.headers) response.content_type = client.content_type @@ -103,9 +126,12 @@ class HassIOView(HomeAssistantView): return response - except ClientError as err: + except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) + except asyncio.TimeoutError: + _LOGGER.error("Client timeout error on API request %s", path) + raise HTTPBadGateway() @@ -125,11 +151,11 @@ def _init_header(request: web.Request) -> dict[str, str]: return headers -def _get_timeout(path: str) -> ClientTimeout: +def _get_timeout(path: str) -> int: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): - return ClientTimeout(connect=10) - return ClientTimeout(connect=10, total=300) + return 0 + return 300 def _need_auth(hass, path: str) -> bool: diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 881d3cc26ed..fc4bb3e6a0d 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,13 +1,11 @@ """The tests for the hassio component.""" -from aiohttp.client import ClientError -from aiohttp.streams import StreamReader -from aiohttp.test_utils import TestClient +import asyncio +from unittest.mock import patch + import pytest from homeassistant.components.hassio.http import _need_auth -from tests.test_util.aiohttp import AiohttpClientMocker - async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" @@ -108,6 +106,16 @@ async def test_forward_log_request(hassio_client, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 +async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): + """Test we get a bad gateway error if we can't find supervisor.""" + with patch( + "homeassistant.components.hassio.http.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + resp = await hassio_client.get("/api/hassio/addons/test/info") + assert resp.status == 502 + + async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mock): """Test that we forward user info correctly.""" aioclient_mock.get("http://127.0.0.1/hello") @@ -163,37 +171,6 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): assert resp.headers["Content-Disposition"] == content_disposition -async def test_supervisor_client_error( - hassio_client: TestClient, aioclient_mock: AiohttpClientMocker -): - """Test any client error from the supervisor returns a 502.""" - # Create a request that throws a ClientError - async def raise_client_error(*args): - raise ClientError() - - aioclient_mock.get( - "http://127.0.0.1/test/raise/error", - side_effect=raise_client_error, - ) - - # Verify it returns bad gateway - resp = await hassio_client.get("/api/hassio/test/raise/error") - assert resp.status == 502 - assert len(aioclient_mock.mock_calls) == 1 - - -async def test_streamed_requests( - hassio_client: TestClient, aioclient_mock: AiohttpClientMocker -): - """Test requests get proxied to the supervisor as a stream.""" - aioclient_mock.get("http://127.0.0.1/test/stream") - await hassio_client.get("/api/hassio/test/stream", data="Test data") - assert len(aioclient_mock.mock_calls) == 1 - - # Verify the request body is passed as a StreamReader - assert isinstance(aioclient_mock.mock_calls[0][2], StreamReader) - - def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") From 630a1fb36ca39eb052f45c78a79974bce02950b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 14:27:55 -0700 Subject: [PATCH 038/199] Bumped version to 2021.8.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c7778e23c67..331b0056c4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 716c3f69ca6b6d26f243551bffa694b67a294463 Mon Sep 17 00:00:00 2001 From: Ryan Johnson <43426700+ryanjohnsontv@users.noreply.github.com> Date: Thu, 29 Jul 2021 23:19:32 -0500 Subject: [PATCH 039/199] Bump pyatv to 0.8.2 (#53659) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index d4eb322f4d7..a726e616641 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.8.1"], + "requirements": ["pyatv==0.8.2"], "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], "codeowners": ["@postlund"], diff --git a/requirements_all.txt b/requirements_all.txt index b7f8ec34eda..3b87599e997 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1326,7 +1326,7 @@ pyatmo==5.2.3 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.8.1 +pyatv==0.8.2 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8b1a136048..9ee06d81b00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -751,7 +751,7 @@ pyatag==0.3.5.3 pyatmo==5.2.3 # homeassistant.components.apple_tv -pyatv==0.8.1 +pyatv==0.8.2 # homeassistant.components.blackbird pyblackbird==0.5 From d54621e778701c4b15f573980f5540b458e8e8db Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 06:50:02 +0200 Subject: [PATCH 040/199] Extract smartthings switch energy attributes into sensors (#53719) --- .../components/smartthings/__init__.py | 3 +- .../components/smartthings/sensor.py | 263 ++++++++++++++---- .../components/smartthings/switch.py | 12 +- tests/components/smartthings/test_sensor.py | 46 ++- tests/components/smartthings/test_switch.py | 8 +- 5 files changed, 257 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 231cfa95263..bc64b173f20 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -9,6 +9,7 @@ import logging from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Attribute, Capability, SmartThings +from pysmartthings.device import DeviceEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -412,7 +413,7 @@ class DeviceBroker: class SmartThingsEntity(Entity): """Defines a SmartThings entity.""" - def __init__(self, device): + def __init__(self, device: DeviceEntity) -> None: """Initialize the instance.""" self._device = device self._dispatcher_remove = None diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index b8bb071fc0a..7a7f9a51855 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -3,18 +3,26 @@ from __future__ import annotations from collections import namedtuple from collections.abc import Sequence +from datetime import datetime from pysmartthings import Attribute, Capability +from pysmartthings.device import DeviceEntity -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, @@ -25,26 +33,27 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.util.dt import utc_from_timestamp from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -Map = namedtuple("map", "attribute name default_unit device_class") +Map = namedtuple("map", "attribute name default_unit device_class state_class") CAPABILITY_TO_SENSORS = { Capability.activity_lighting_mode: [ - Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None) + Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None, None) ], Capability.air_conditioner_mode: [ - Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None) + Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None, None) ], Capability.air_quality_sensor: [ - Map(Attribute.air_quality, "Air Quality", "CAQI", None) + Map(Attribute.air_quality, "Air Quality", "CAQI", None, STATE_CLASS_MEASUREMENT) ], - Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None)], - Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None)], + Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None)], + Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None, None)], Capability.battery: [ - Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY) + Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY, None) ], Capability.body_mass_index_measurement: [ Map( @@ -52,57 +61,80 @@ CAPABILITY_TO_SENSORS = { "Body Mass Index", f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}", None, + STATE_CLASS_MEASUREMENT, ) ], Capability.body_weight_measurement: [ - Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None) + Map( + Attribute.body_weight_measurement, + "Body Weight", + MASS_KILOGRAMS, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.carbon_dioxide_measurement: [ Map( Attribute.carbon_dioxide, "Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - None, + DEVICE_CLASS_CO2, + STATE_CLASS_MEASUREMENT, ) ], Capability.carbon_monoxide_detector: [ - Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None) + Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None, None) ], Capability.carbon_monoxide_measurement: [ Map( Attribute.carbon_monoxide_level, "Carbon Monoxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - None, + DEVICE_CLASS_CO, + STATE_CLASS_MEASUREMENT, ) ], Capability.dishwasher_operating_state: [ - Map(Attribute.machine_state, "Dishwasher Machine State", None, None), - Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None), + Map(Attribute.machine_state, "Dishwasher Machine State", None, None, None), + Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None, None), Map( Attribute.completion_time, "Dishwasher Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], - Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None)], + Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None, None)], Capability.dryer_operating_state: [ - Map(Attribute.machine_state, "Dryer Machine State", None, None), - Map(Attribute.dryer_job_state, "Dryer Job State", None, None), + Map(Attribute.machine_state, "Dryer Machine State", None, None, None), + Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None), Map( Attribute.completion_time, "Dryer Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], Capability.dust_sensor: [ - Map(Attribute.fine_dust_level, "Fine Dust Level", None, None), - Map(Attribute.dust_level, "Dust Level", None, None), + Map( + Attribute.fine_dust_level, + "Fine Dust Level", + None, + None, + STATE_CLASS_MEASUREMENT, + ), + Map(Attribute.dust_level, "Dust Level", None, None, STATE_CLASS_MEASUREMENT), ], Capability.energy_meter: [ - Map(Attribute.energy, "Energy Meter", ENERGY_KILO_WATT_HOUR, None) + Map( + Attribute.energy, + "Energy Meter", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + ) ], Capability.equivalent_carbon_dioxide_measurement: [ Map( @@ -110,6 +142,7 @@ CAPABILITY_TO_SENSORS = { "Equivalent Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.formaldehyde_measurement: [ @@ -118,50 +151,94 @@ CAPABILITY_TO_SENSORS = { "Formaldehyde Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.gas_meter: [ - Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None), - Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None), - Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP), - Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None), + Map( + Attribute.gas_meter, + "Gas Meter", + ENERGY_KILO_WATT_HOUR, + None, + STATE_CLASS_MEASUREMENT, + ), + Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None), + Map( + Attribute.gas_meter_time, + "Gas Meter Time", + None, + DEVICE_CLASS_TIMESTAMP, + None, + ), + Map( + Attribute.gas_meter_volume, + "Gas Meter Volume", + VOLUME_CUBIC_METERS, + None, + STATE_CLASS_MEASUREMENT, + ), ], Capability.illuminance_measurement: [ - Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE) + Map( + Attribute.illuminance, + "Illuminance", + LIGHT_LUX, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ) ], Capability.infrared_level: [ - Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None) + Map( + Attribute.infrared_level, + "Infrared Level", + PERCENTAGE, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.media_input_source: [ - Map(Attribute.input_source, "Media Input Source", None, None) + Map(Attribute.input_source, "Media Input Source", None, None, None) ], Capability.media_playback_repeat: [ - Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None) + Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None, None) ], Capability.media_playback_shuffle: [ - Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None) + Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None) ], Capability.media_playback: [ - Map(Attribute.playback_status, "Media Playback Status", None, None) + Map(Attribute.playback_status, "Media Playback Status", None, None, None) ], - Capability.odor_sensor: [Map(Attribute.odor_level, "Odor Sensor", None, None)], - Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None)], + Capability.odor_sensor: [ + Map(Attribute.odor_level, "Odor Sensor", None, None, None) + ], + Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None, None)], Capability.oven_operating_state: [ - Map(Attribute.machine_state, "Oven Machine State", None, None), - Map(Attribute.oven_job_state, "Oven Job State", None, None), - Map(Attribute.completion_time, "Oven Completion Time", None, None), + Map(Attribute.machine_state, "Oven Machine State", None, None, None), + Map(Attribute.oven_job_state, "Oven Job State", None, None, None), + Map(Attribute.completion_time, "Oven Completion Time", None, None, None), ], Capability.oven_setpoint: [ - Map(Attribute.oven_setpoint, "Oven Set Point", None, None) + Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None) + ], + Capability.power_meter: [ + Map( + Attribute.power, + "Power Meter", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ) + ], + Capability.power_source: [ + Map(Attribute.power_source, "Power Source", None, None, None) ], - Capability.power_meter: [Map(Attribute.power, "Power Meter", POWER_WATT, None)], - Capability.power_source: [Map(Attribute.power_source, "Power Source", None, None)], Capability.refrigeration_setpoint: [ Map( Attribute.refrigeration_setpoint, "Refrigeration Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.relative_humidity_measurement: [ @@ -170,6 +247,7 @@ CAPABILITY_TO_SENSORS = { "Relative Humidity Measurement", PERCENTAGE, DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, ) ], Capability.robot_cleaner_cleaning_mode: [ @@ -178,25 +256,43 @@ CAPABILITY_TO_SENSORS = { "Robot Cleaner Cleaning Mode", None, None, + None, ) ], Capability.robot_cleaner_movement: [ - Map(Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None) + Map( + Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None, None + ) ], Capability.robot_cleaner_turbo_mode: [ - Map(Attribute.robot_cleaner_turbo_mode, "Robot Cleaner Turbo Mode", None, None) + Map( + Attribute.robot_cleaner_turbo_mode, + "Robot Cleaner Turbo Mode", + None, + None, + None, + ) ], Capability.signal_strength: [ - Map(Attribute.lqi, "LQI Signal Strength", None, None), - Map(Attribute.rssi, "RSSI Signal Strength", None, None), + Map(Attribute.lqi, "LQI Signal Strength", None, None, STATE_CLASS_MEASUREMENT), + Map( + Attribute.rssi, + "RSSI Signal Strength", + None, + DEVICE_CLASS_SIGNAL_STRENGTH, + STATE_CLASS_MEASUREMENT, + ), + ], + Capability.smoke_detector: [ + Map(Attribute.smoke, "Smoke Detector", None, None, None) ], - Capability.smoke_detector: [Map(Attribute.smoke, "Smoke Detector", None, None)], Capability.temperature_measurement: [ Map( Attribute.temperature, "Temperature Measurement", None, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, ) ], Capability.thermostat_cooling_setpoint: [ @@ -205,10 +301,11 @@ CAPABILITY_TO_SENSORS = { "Thermostat Cooling Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.thermostat_fan_mode: [ - Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None) + Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None, None) ], Capability.thermostat_heating_setpoint: [ Map( @@ -216,10 +313,11 @@ CAPABILITY_TO_SENSORS = { "Thermostat Heating Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.thermostat_mode: [ - Map(Attribute.thermostat_mode, "Thermostat Mode", None, None) + Map(Attribute.thermostat_mode, "Thermostat Mode", None, None, None) ], Capability.thermostat_operating_state: [ Map( @@ -227,6 +325,7 @@ CAPABILITY_TO_SENSORS = { "Thermostat Operating State", None, None, + None, ) ], Capability.thermostat_setpoint: [ @@ -235,12 +334,13 @@ CAPABILITY_TO_SENSORS = { "Thermostat Setpoint", None, DEVICE_CLASS_TEMPERATURE, + None, ) ], Capability.three_axis: [], Capability.tv_channel: [ - Map(Attribute.tv_channel, "Tv Channel", None, None), - Map(Attribute.tv_channel_name, "Tv Channel Name", None, None), + Map(Attribute.tv_channel, "Tv Channel", None, None, None), + Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None), ], Capability.tvoc_measurement: [ Map( @@ -248,23 +348,39 @@ CAPABILITY_TO_SENSORS = { "Tvoc Measurement", CONCENTRATION_PARTS_PER_MILLION, None, + STATE_CLASS_MEASUREMENT, ) ], Capability.ultraviolet_index: [ - Map(Attribute.ultraviolet_index, "Ultraviolet Index", None, None) + Map( + Attribute.ultraviolet_index, + "Ultraviolet Index", + None, + None, + STATE_CLASS_MEASUREMENT, + ) ], Capability.voltage_measurement: [ - Map(Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, None) + Map( + Attribute.voltage, + "Voltage Measurement", + ELECTRIC_POTENTIAL_VOLT, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ) + ], + Capability.washer_mode: [ + Map(Attribute.washer_mode, "Washer Mode", None, None, None) ], - Capability.washer_mode: [Map(Attribute.washer_mode, "Washer Mode", None, None)], Capability.washer_operating_state: [ - Map(Attribute.machine_state, "Washer Machine State", None, None), - Map(Attribute.washer_job_state, "Washer Job State", None, None), + Map(Attribute.machine_state, "Washer Machine State", None, None, None), + Map(Attribute.washer_job_state, "Washer Job State", None, None, None), Map( Attribute.completion_time, "Washer Completion Time", None, DEVICE_CLASS_TIMESTAMP, + None, ), ], } @@ -292,11 +408,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.extend( [ SmartThingsSensor( - device, m.attribute, m.name, m.default_unit, m.device_class + device, + m.attribute, + m.name, + m.default_unit, + m.device_class, + m.state_class, ) for m in maps ] ) + + if broker.any_assigned(device.device_id, "switch"): + for capability in (Capability.energy_meter, Capability.power_meter): + maps = CAPABILITY_TO_SENSORS[capability] + sensors.extend( + [ + SmartThingsSensor( + device, + m.attribute, + m.name, + m.default_unit, + m.device_class, + m.state_class, + ) + for m in maps + ] + ) + async_add_entities(sensors) @@ -311,14 +450,21 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Sensor.""" def __init__( - self, device, attribute: str, name: str, default_unit: str, device_class: str - ): + self, + device: DeviceEntity, + attribute: str, + name: str, + default_unit: str, + device_class: str, + state_class: str | None, + ) -> None: """Init the class.""" super().__init__(device) self._attribute = attribute self._name = name self._device_class = device_class self._default_unit = default_unit + self._attr_state_class = state_class @property def name(self) -> str: @@ -346,6 +492,13 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): unit = self._device.status.attributes[self._attribute].unit return UNITS.get(unit, unit) if unit else self._default_unit + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + if self._attribute == Attribute.energy: + return utc_from_timestamp(0) + return None + class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Three Axis Sensor.""" diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 7b8364d9ba3..7e92ba4f663 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from pysmartthings import Attribute, Capability +from pysmartthings import Capability from homeassistant.components.switch import SwitchEntity @@ -48,16 +48,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self._device.status.attributes[Attribute.power].value - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self._device.status.attributes[Attribute.energy].value - @property def is_on(self) -> bool: """Return true if light is on.""" diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 4af88e27fe4..ffb577c903a 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -6,7 +6,11 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability -from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASSES, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASSES, +) from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState @@ -33,6 +37,8 @@ async def test_mapping_integrity(): assert ( sensor_map.device_class in DEVICE_CLASSES ), sensor_map.device_class + if sensor_map.state_class: + assert sensor_map.state_class in STATE_CLASSES, sensor_map.state_class async def test_entity_state(hass, device_factory): @@ -95,6 +101,44 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry.manufacturer == "Unavailable" +async def test_energy_sensors_for_switch_device(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "Switch_1", + [Capability.switch, Capability.power_meter, Capability.energy_meter], + {Attribute.switch: "off", Attribute.power: 355, Attribute.energy: 11.422}, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.switch_1_energy_meter") + assert state + assert state.state == "11.422" + entry = entity_registry.async_get("sensor.switch_1_energy_meter") + assert entry + assert entry.unique_id == f"{device.device_id}.{Attribute.energy}" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + state = hass.states.get("sensor.switch_1_power_meter") + assert state + assert state.state == "355" + entry = entity_registry.async_get("sensor.switch_1_power_meter") + assert entry + assert entry.unique_id == f"{device.device_id}.{Attribute.power}" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" # Arrange diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 7c202fad12e..c884d601baf 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -7,11 +7,7 @@ real HTTP calls are not initiated during testing. from pysmartthings import Attribute, Capability from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -72,8 +68,6 @@ async def test_turn_on(hass, device_factory): state = hass.states.get("switch.switch_1") assert state is not None assert state.state == "on" - assert state.attributes[ATTR_CURRENT_POWER_W] == 355 - assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422 async def test_update_from_signal(hass, device_factory): From bfacff5d78186d7e1c41ced62dbf4f5f1950cf93 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Jul 2021 01:16:08 +0200 Subject: [PATCH 041/199] Add energy device class to deCONZ consumption sensors (#53731) --- homeassistant/components/deconz/sensor.py | 7 +++++++ tests/components/deconz/test_sensor.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index e0f12303946..9282f2d26cc 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -40,6 +41,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.util import dt as dt_util from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -51,6 +53,7 @@ ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" DEVICE_CLASS = { + Consumption: DEVICE_CLASS_ENERGY, Humidity: DEVICE_CLASS_HUMIDITY, LightLevel: DEVICE_CLASS_ILLUMINANCE, Power: DEVICE_CLASS_POWER, @@ -65,6 +68,7 @@ ICON = { } STATE_CLASS = { + Consumption: STATE_CLASS_MEASUREMENT, Humidity: STATE_CLASS_MEASUREMENT, Pressure: STATE_CLASS_MEASUREMENT, Temperature: STATE_CLASS_MEASUREMENT, @@ -158,6 +162,9 @@ class DeconzSensor(DeconzDevice, SensorEntity): self._attr_state_class = STATE_CLASS.get(type(self._device)) self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device)) + if device.type in Consumption.ZHATYPE: + self._attr_last_reset = dt_util.utc_from_timestamp(0) + @callback def async_update_callback(self, force_update=False): """Update the sensor's state.""" diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index d9c4adf1388..7f8bce24d80 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, @@ -118,7 +119,7 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" - assert ATTR_DEVICE_CLASS not in consumption_sensor.attributes + assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert not hass.states.get("sensor.clip_light_level_sensor") From 37c3062874bef5a2c38c9a06b9252599043979e9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 29 Jul 2021 19:16:47 -0400 Subject: [PATCH 042/199] Bump up ZHA dependencies (#53732) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8df2ddeb30..fa117a3f1ff 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,7 +10,7 @@ "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.36.0", + "zigpy==0.36.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.2" diff --git a/requirements_all.txt b/requirements_all.txt index 3b87599e997..75313a53947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.36.0 +zigpy==0.36.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ee06d81b00..7a49e3faae2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1362,7 +1362,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.2 # homeassistant.components.zha -zigpy==0.36.0 +zigpy==0.36.1 # homeassistant.components.zwave_js zwave-js-server-python==0.28.0 From 0442827b9ecf0cf169182e8a0501f8312127237a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 06:18:09 +0200 Subject: [PATCH 043/199] Fix exception handling in DataUpdateCoordinator in TP-Link (#53734) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/__init__.py | 81 +++++++++++---------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 9d5263687f8..e309d2c5082 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utc_from_timestamp from .common import SmartDevices, async_discover_devices, get_static_devices @@ -185,45 +185,50 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict: """Fetch all device and sensor data from api.""" - info = self.smartplug.sys_info - data = { - CONF_HOST: self.smartplug.host, - CONF_MAC: info["mac"], - CONF_MODEL: info["model"], - CONF_SW_VERSION: info["sw_ver"], - } - if self.smartplug.context is None: - data[CONF_ALIAS] = info["alias"] - data[CONF_DEVICE_ID] = info["mac"] - data[CONF_STATE] = self.smartplug.state == self.smartplug.SWITCH_STATE_ON - else: - plug_from_context = next( - c - for c in self.smartplug.sys_info["children"] - if c["id"] == self.smartplug.context - ) - data[CONF_ALIAS] = plug_from_context["alias"] - data[CONF_DEVICE_ID] = self.smartplug.context - data[CONF_STATE] = plug_from_context["state"] == 1 - if self.smartplug.has_emeter: - emeter_readings = self.smartplug.get_emeter_realtime() - data[CONF_EMETER_PARAMS] = { - ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), - ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), - ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), - ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), - ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + try: + info = self.smartplug.sys_info + data = { + CONF_HOST: self.smartplug.host, + CONF_MAC: info["mac"], + CONF_MODEL: info["model"], + CONF_SW_VERSION: info["sw_ver"], } - emeter_statics = self.smartplug.get_emeter_daily() - data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TODAY_ENERGY_KWH - ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - if emeter_statics.get(int(time.strftime("%e"))): - data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( - float(emeter_statics[int(time.strftime("%e"))]), 3 + if self.smartplug.context is None: + data[CONF_ALIAS] = info["alias"] + data[CONF_DEVICE_ID] = info["mac"] + data[CONF_STATE] = ( + self.smartplug.state == self.smartplug.SWITCH_STATE_ON ) else: - # today's consumption not available, when device was off all the day - data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + plug_from_context = next( + c + for c in self.smartplug.sys_info["children"] + if c["id"] == self.smartplug.context + ) + data[CONF_ALIAS] = plug_from_context["alias"] + data[CONF_DEVICE_ID] = self.smartplug.context + data[CONF_STATE] = plug_from_context["state"] == 1 + if self.smartplug.has_emeter: + emeter_readings = self.smartplug.get_emeter_realtime() + data[CONF_EMETER_PARAMS] = { + ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), + ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), + ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), + ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), + ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, + } + emeter_statics = self.smartplug.get_emeter_daily() + data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ + ATTR_TODAY_ENERGY_KWH + ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + if emeter_statics.get(int(time.strftime("%e"))): + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( + float(emeter_statics[int(time.strftime("%e"))]), 3 + ) + else: + # today's consumption not available, when device was off all the day + data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0 + except SmartDeviceException as ex: + raise UpdateFailed(ex) from ex return data From d2dfdd81adff1ec13cade4cb1c472e9f190ec208 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 30 Jul 2021 01:08:52 -0400 Subject: [PATCH 044/199] Only allow one Mazda vehicle status request at a time (#53736) --- homeassistant/components/mazda/__init__.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 921dd4c06c5..c64a3b35993 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -28,7 +28,6 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, UpdateFailed, ) -from homeassistant.util.async_ import gather_with_concurrency from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES @@ -143,14 +142,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: vehicles = await with_timeout(mazda_client.get_vehicles()) - vehicle_status_tasks = [ - with_timeout(mazda_client.get_vehicle_status(vehicle["id"])) - for vehicle in vehicles - ] - statuses = await gather_with_concurrency(5, *vehicle_status_tasks) - - for vehicle, status in zip(vehicles, statuses): - vehicle["status"] = status + # The Mazda API can throw an error when multiple simultaneous requests are + # made for the same account, so we can only make one request at a time here + for vehicle in vehicles: + vehicle["status"] = await with_timeout( + mazda_client.get_vehicle_status(vehicle["id"]) + ) hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles From 128dc07fa5388852cdd60730dcd86249a674cfaa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Jul 2021 07:11:15 +0200 Subject: [PATCH 045/199] Apply left suggestions #53596 for TP-Link (#53737) --- homeassistant/components/tplink/const.py | 55 ------------- homeassistant/components/tplink/sensor.py | 62 ++++++++++++++- homeassistant/components/tplink/switch.py | 4 +- tests/components/tplink/test_init.py | 95 +++++++---------------- 4 files changed, 91 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 93cad889a2f..888d671096d 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -3,23 +3,6 @@ from __future__ import annotations import datetime -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH -from homeassistant.const import ( - ATTR_VOLTAGE, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) - DOMAIN = "tplink" COORDINATORS = "coordinators" @@ -41,41 +24,3 @@ CONF_SWITCH = "switch" CONF_SENSOR = "sensor" PLATFORMS = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH] - -ENERGY_SENSORS: list[SensorEntityDescription] = [ - SensorEntityDescription( - key=ATTR_CURRENT_POWER_W, - unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - name="Current Consumption", - ), - SensorEntityDescription( - key=ATTR_TOTAL_ENERGY_KWH, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - name="Total Consumption", - ), - SensorEntityDescription( - key=ATTR_TODAY_ENERGY_KWH, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - name="Today's Consumption", - ), - SensorEntityDescription( - key=ATTR_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - name="Voltage", - ), - SensorEntityDescription( - key=ATTR_CURRENT_A, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - name="Current", - ), -] diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 24b93e90963..bb6596b82d1 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,18 +1,32 @@ """Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" from __future__ import annotations -from typing import Any +from typing import Any, Final from pyHS100 import SmartPlug from homeassistant.components.sensor import ( ATTR_LAST_RESET, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ALIAS, + CONF_DEVICE_ID, + CONF_MAC, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -29,9 +43,51 @@ from .const import ( CONF_SWITCH, COORDINATORS, DOMAIN as TPLINK_DOMAIN, - ENERGY_SENSORS, ) +ATTR_CURRENT_A = "current_a" +ATTR_CURRENT_POWER_W = "current_power_w" +ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" +ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" + +ENERGY_SENSORS: Final[list[SensorEntityDescription]] = [ + SensorEntityDescription( + key=ATTR_CURRENT_POWER_W, + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + name="Current Consumption", + ), + SensorEntityDescription( + key=ATTR_TOTAL_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Total Consumption", + ), + SensorEntityDescription( + key=ATTR_TODAY_ENERGY_KWH, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + name="Today's Consumption", + ), + SensorEntityDescription( + key=ATTR_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + name="Voltage", + ), + SensorEntityDescription( + key=ATTR_CURRENT_A, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + name="Current", + ), +] + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 688091991c3..f5319de999a 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -88,10 +88,10 @@ class SmartPlugSwitch(CoordinatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.hass.async_add_job(self.smartplug.turn_on) + await self.hass.async_add_executor_job(self.smartplug.turn_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.hass.async_add_job(self.smartplug.turn_off) + await self.hass.async_add_executor_job(self.smartplug.turn_off) await self.coordinator.async_refresh() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 0cfb4d3d233..fb3f44709fc 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,7 +1,6 @@ """Tests for the TP-Link component.""" from __future__ import annotations -from datetime import datetime import time from typing import Any from unittest.mock import MagicMock, patch @@ -12,31 +11,21 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink -from homeassistant.components.sensor import ATTR_LAST_RESET -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH from homeassistant.components.tplink.common import SmartDevices from homeassistant.components.tplink.const import ( - ATTR_CURRENT_A, - ATTR_TOTAL_ENERGY_KWH, CONF_DIMMER, CONF_DISCOVERY, - CONF_EMETER_PARAMS, CONF_LIGHT, - CONF_MODEL, CONF_SW_VERSION, CONF_SWITCH, COORDINATORS, ) -from homeassistant.const import ( - ATTR_VOLTAGE, - CONF_ALIAS, - CONF_DEVICE_ID, - CONF_HOST, - CONF_MAC, -) +from homeassistant.components.tplink.sensor import ENERGY_SENSORS +from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util import slugify from tests.common import MockConfigEntry, mock_coro from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA @@ -217,20 +206,16 @@ async def test_platforms_are_initialized(hass: HomeAssistant): } } - with patch( - "homeassistant.components.tplink.common.Discover.discover" - ) as discover, patch( + with patch("homeassistant.components.tplink.common.Discover.discover"), patch( "homeassistant.components.tplink.get_static_devices" ) as get_static_devices, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ) as light_setup, patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), - ) as switch_setup, patch( - "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", + False, ): light = SmartBulb("123.123.123.123") @@ -248,47 +233,25 @@ async def test_platforms_are_initialized(hass: HomeAssistant): await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert hass.data.get(tplink.DOMAIN) - assert hass.data[tplink.DOMAIN].get(COORDINATORS) - assert hass.data[tplink.DOMAIN][COORDINATORS].get(switch.mac) - assert isinstance( - hass.data[tplink.DOMAIN][COORDINATORS][switch.mac], - tplink.SmartPlugDataUpdateCoordinator, - ) - data = hass.data[tplink.DOMAIN][COORDINATORS][switch.mac].data - assert data[CONF_HOST] == switch.host - assert data[CONF_MAC] == switch.sys_info["mac"] - assert data[CONF_MODEL] == switch.sys_info["model"] - assert data[CONF_SW_VERSION] == switch.sys_info["sw_ver"] - assert data[CONF_ALIAS] == switch.sys_info["alias"] - assert data[CONF_DEVICE_ID] == switch.sys_info["mac"] + state = hass.states.get(f"switch.{switch.alias}") + assert state + assert state.name == switch.alias - emeter_readings = switch.get_emeter_realtime() - assert data[CONF_EMETER_PARAMS][ATTR_VOLTAGE] == round( - float(emeter_readings["voltage"]), 1 - ) - assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_A] == round( - float(emeter_readings["current"]), 2 - ) - assert data[CONF_EMETER_PARAMS][ATTR_CURRENT_POWER_W] == round( - float(emeter_readings["power"]), 2 - ) - assert data[CONF_EMETER_PARAMS][ATTR_TOTAL_ENERGY_KWH] == round( - float(emeter_readings["total"]), 3 - ) - assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TOTAL_ENERGY_KWH - ] == utc_from_timestamp(0) + for description in ENERGY_SENSORS: + state = hass.states.get( + f"sensor.{switch.alias}_{slugify(description.name)}" + ) + assert state + assert state.state is not None + assert state.name == f"{switch.alias} {description.name}" - assert data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] == 1.123 - assert data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TODAY_ENERGY_KWH - ] == datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - - assert discover.call_count == 0 - assert get_static_devices.call_count == 1 - assert light_setup.call_count == 1 - assert switch_setup.call_count == 1 + device_registry = dr.async_get(hass) + assert len(device_registry.devices) == 1 + device = next(iter(device_registry.devices.values())) + assert device.name == switch.alias + assert device.model == switch.model + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, switch.mac.lower())} + assert device.sw_version == switch.sys_info[CONF_SW_VERSION] async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): @@ -311,8 +274,6 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): "homeassistant.components.tplink.switch.async_setup_entry", return_value=mock_coro(True), ), patch( - "homeassistant.components.tplink.sensor.SmartPlugSensor.__init__" - ) as SmartPlugSensor, patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): @@ -323,7 +284,11 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert SmartPlugSensor.call_count == 0 + for description in ENERGY_SENSORS: + state = hass.states.get( + f"sensor.{switch.alias}_{slugify(description.name)}" + ) + assert state is None async def test_smartstrip_device(hass: HomeAssistant): From 83e4e4f769f810939cfbed2eda30223236ba373e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 07:10:16 +0200 Subject: [PATCH 046/199] Fix Xiaomi humidifier name migration (#53738) --- homeassistant/components/xiaomi_miio/__init__.py | 6 ++---- homeassistant/components/xiaomi_miio/const.py | 1 - homeassistant/components/xiaomi_miio/humidifier.py | 6 +----- homeassistant/components/xiaomi_miio/number.py | 7 +------ homeassistant/components/xiaomi_miio/select.py | 7 +------ homeassistant/components/xiaomi_miio/sensor.py | 9 ++------- homeassistant/components/xiaomi_miio/switch.py | 7 +------ 7 files changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 4ca64fc175c..e858c7ee797 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -21,7 +21,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODELS_AIR_MONITOR, MODELS_FAN, MODELS_HUMIDIFIER, @@ -112,12 +111,13 @@ async def async_create_miio_device_and_coordinator( else: device = AirHumidifier(host, token, model=model) - # Removing fan platform entity for humidifiers and cache the name and entity name for migration + # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration entity_registry = er.async_get(hass) entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future migrate_entity_name = entity_registry.async_get(entity_id).name + hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) async def async_update_data(): @@ -142,8 +142,6 @@ async def async_create_miio_device_and_coordinator( KEY_DEVICE: device, KEY_COORDINATOR: coordinator, } - if migrate_entity_name: - hass.data[DOMAIN][entry.entry_id][KEY_MIGRATE_ENTITY_NAME] = migrate_entity_name # Trigger first data fetch await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 05499efb5d3..0d8d5bc0014 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -18,7 +18,6 @@ CONF_CLOUD_SUBDEVICES = "cloud_subdevices" # Keys KEY_COORDINATOR = "coordinator" KEY_DEVICE = "device" -KEY_MIGRATE_ENTITY_NAME = "migrate_entity_name" # Attributes ATTR_AVAILABLE = "available" diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 9d9f49229d1..eb45a716254 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -24,7 +24,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -52,10 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title + name = config_entry.title if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 4d0e92b104f..6855faa6391 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -13,7 +13,6 @@ from .const import ( FEATURE_SET_MOTOR_SPEED, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA4, ) from .device import XiaomiCoordinatedMiioEntity @@ -58,10 +57,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if model not in [MODEL_AIRHUMIDIFIER_CA4]: return @@ -69,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for number in NUMBER_TYPES.values(): entities.append( XiaomiAirHumidifierNumber( - f"{name} {number.name}", + f"{config_entry.title} {number.name}", device, config_entry, f"{number.short_name}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 055d8073739..77aba961244 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -16,7 +16,6 @@ from .const import ( FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -67,10 +66,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: entity_class = XiaomiAirHumidifierSelector @@ -84,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for selector in SELECTOR_TYPES.values(): entities.append( entity_class( - f"{name} {selector.name}", + f"{config_entry.title} {selector.name}", device, config_entry, f"{selector.short_name}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 3c28d8496e7..413971aa880 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -45,7 +45,6 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODELS_HUMIDIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity @@ -190,11 +189,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = None sensors = [] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title - if model in MODELS_HUMIDIFIER_MIOT: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -205,6 +199,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = HUMIDIFIER_SENSORS else: unique_id = config_entry.unique_id + name = config_entry.title _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) device = AirQualityMonitor(host, token) @@ -214,7 +209,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: entities.append( XiaomiGenericSensor( - f"{name} {sensor.replace('_', ' ').title()}", + f"{config_entry.title} {sensor.replace('_', ' ').title()}", device, config_entry, f"{sensor}_{config_entry.unique_id}", diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 35ac4c7da4f..c86c98de34c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -41,7 +41,6 @@ from .const import ( FEATURE_SET_DRY, KEY_COORDINATOR, KEY_DEVICE, - KEY_MIGRATE_ENTITY_NAME, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -235,10 +234,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]: - name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME] - else: - name = config_entry.title if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -256,7 +251,7 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): if feature & device_features: entities.append( XiaomiGenericCoordinatedSwitch( - f"{name} {switch.name}", + f"{config_entry.title} {switch.name}", device, config_entry, f"{switch.short_name}_{unique_id}", From d34bd8ad1e43f548d4908c140ef0ef73a1834136 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 07:10:41 +0200 Subject: [PATCH 047/199] Fix Xiaomi-miio switch platform setup (#53739) --- homeassistant/components/xiaomi_miio/switch.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index c86c98de34c..bdf3085f236 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -218,13 +218,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the switch from a config entry.""" - if ( - config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY - or config_entry.data[CONF_MODEL] == "lumi.acpartner.v3" - ): - await async_setup_other_entry(hass, config_entry, async_add_entities) - else: + if config_entry.data[CONF_MODEL] in MODELS_HUMIDIFIER: await async_setup_coordinated_entry(hass, config_entry, async_add_entities) + else: + await async_setup_other_entry(hass, config_entry, async_add_entities) async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): From 9dcd3f662693d00d769dd95ec2af3c9bf3ade214 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 23:34:03 -0700 Subject: [PATCH 048/199] Add energy attributes to Fronius (#53741) * Add energy attributes to Fronius * Add solar * Add power * Only add last reset for total meter entities * Only add last reset for total solar entities * Create different entity descriptions per key * only return the entity description for energy total * Use correct key * Meter devices keep it real * keys start with energy_real * Also device key starts with * Lint --- homeassistant/components/fronius/sensor.py | 117 ++++++++++++++++----- 1 file changed, 88 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ac006638912..6f949334d02 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -8,17 +8,26 @@ import logging from pyfronius import Fronius import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_DEVICE, CONF_MONITORED_CONDITIONS, CONF_RESOURCE, CONF_SCAN_INTERVAL, CONF_SENSOR_TYPE, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, ) +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -152,6 +161,12 @@ class FroniusAdapter: """Whether the fronius device is active.""" return self._available + def entity_description( # pylint: disable=no-self-use + self, key + ) -> SensorEntityDescription | None: + """Create entity description for a key.""" + return None + async def async_update(self): """Retrieve and update latest state.""" try: @@ -198,14 +213,28 @@ class FroniusAdapter: async def _update(self) -> dict: """Return values of interest.""" - async def register(self, sensor): + @callback + def register(self, sensor): """Register child sensor for update subscriptions.""" self._registered_sensors.add(sensor) + return lambda: self._registered_sensors.remove(sensor) class FroniusInverterSystem(FroniusAdapter): """Adapter for the fronius inverter with system scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if key != "energy_total": + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_inverter_data() @@ -214,6 +243,18 @@ class FroniusInverterSystem(FroniusAdapter): class FroniusInverterDevice(FroniusAdapter): """Adapter for the fronius inverter with device scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if key != "energy_total": + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_inverter_data(self._device) @@ -230,6 +271,18 @@ class FroniusStorage(FroniusAdapter): class FroniusMeterSystem(FroniusAdapter): """Adapter for the fronius meter with system scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if not key.startswith("energy_real_"): + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_meter_data() @@ -238,6 +291,18 @@ class FroniusMeterSystem(FroniusAdapter): class FroniusMeterDevice(FroniusAdapter): """Adapter for the fronius meter with device scope.""" + def entity_description(self, key): + """Return the entity descriptor.""" + if not key.startswith("energy_real_"): + return None + + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_meter_data(self._device) @@ -246,6 +311,14 @@ class FroniusMeterDevice(FroniusAdapter): class FroniusPowerFlow(FroniusAdapter): """Adapter for the fronius power flow.""" + def entity_description(self, key): + """Return the entity descriptor.""" + return SensorEntityDescription( + key=key, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ) + async def _update(self): """Get the values for the current state.""" return await self.bridge.current_power_flow() @@ -254,27 +327,13 @@ class FroniusPowerFlow(FroniusAdapter): class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" - def __init__(self, parent: FroniusAdapter, name): + def __init__(self, parent: FroniusAdapter, key): """Initialize a singular value sensor.""" - self._name = name - self.parent = parent - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name.replace('_', ' ').capitalize()} {self.parent.name}" - - @property - def state(self): - """Return the current state.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit + self._key = key + self._attr_name = f"{key.replace('_', ' ').capitalize()} {parent.name}" + self._parent = parent + if entity_description := parent.entity_description(key): + self.entity_description = entity_description @property def should_poll(self): @@ -284,19 +343,19 @@ class FroniusTemplateSensor(SensorEntity): @property def available(self): """Whether the fronius device is active.""" - return self.parent.available + return self._parent.available async def async_update(self): """Update the internal state.""" - state = self.parent.data.get(self._name) - self._state = state.get("value") - if isinstance(self._state, float): - self._state = round(self._state, 2) - self._unit = state.get("unit") + state = self._parent.data.get(self._key) + self._attr_state = state.get("value") + if isinstance(self._attr_state, float): + self._attr_state = round(self._attr_state, 2) + self._attr_unit_of_measurement = state.get("unit") async def async_added_to_hass(self): """Register at parent component for updates.""" - await self.parent.register(self) + self.async_on_remove(self._parent.register(self)) def __hash__(self): """Hash sensor by hashing its name.""" From 447901c22370b7ae2d41932d2648db29d69bc038 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jul 2021 23:35:09 -0700 Subject: [PATCH 049/199] Bumped version to 2021.8.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 331b0056c4e..bc79728c307 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From dac968bf32b3991357e21c85b4b29a2689ab1c98 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 30 Jul 2021 23:02:33 +0800 Subject: [PATCH 050/199] Fix non monotonic dts error in stream (#53712) * Use defaultdict for TimestampValidator._last_dts * Combine filters * Allow PeekIterator to be updated while preserving buffer * Fix peek edge case * Readd is_valid filter to video only iterator --- homeassistant/components/stream/worker.py | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 72625fa0f5a..69def43b2a2 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -1,7 +1,7 @@ """Provides the worker thread needed for processing streams.""" from __future__ import annotations -from collections import deque +from collections import defaultdict, deque from collections.abc import Generator, Iterator, Mapping from io import BytesIO import logging @@ -222,6 +222,12 @@ class PeekIterator(Iterator): """Return and consume the next item available.""" return self._next() + def replace_underlying_iterator(self, new_iterator: Iterator) -> None: + """Replace the underlying iterator while preserving the buffer.""" + self._iterator = new_iterator + if self._next is not self._pop_buffer: + self._next = self._iterator.__next__ + def _pop_buffer(self) -> av.Packet: """Consume items from the buffer until exhausted.""" if self._buffer: @@ -248,7 +254,9 @@ class TimestampValidator: def __init__(self) -> None: """Initialize the TimestampValidator.""" # Decompression timestamp of last packet in each stream - self._last_dts: dict[av.stream.Stream, float] = {} + self._last_dts: dict[av.stream.Stream, int | float] = defaultdict( + lambda: float("-inf") + ) # Number of consecutive missing decompression timestamps self._missing_dts = 0 @@ -264,7 +272,7 @@ class TimestampValidator: return False self._missing_dts = 0 # Discard when dts is not monotonic. Terminate if gap is too wide. - prev_dts = self._last_dts.get(packet.stream, float("-inf")) + prev_dts = self._last_dts[packet.stream] if packet.dts <= prev_dts: gap = packet.time_base * (prev_dts - packet.dts) if gap > MAX_TIMESTAMP_GAP: @@ -350,19 +358,25 @@ def stream_worker( try: if audio_stream and unsupported_audio(container_packets.peek(), audio_stream): audio_stream = None - container_packets = PeekIterator( + container_packets.replace_underlying_iterator( filter(dts_validator.is_valid, container.demux(video_stream)) ) # Advance to the first keyframe for muxing, then rewind so the muxing # loop below can consume. - first_keyframe = next(filter(is_keyframe, filter(is_video, container_packets))) + first_keyframe = next( + filter(lambda pkt: is_keyframe(pkt) and is_video(pkt), container_packets) + ) # Deal with problem #1 above (bad first packet pts/dts) by recalculating # using pts/dts from second packet. Use the peek iterator to advance # without consuming from container_packets. Skip over the first keyframe # then use the duration from the second video packet to adjust dts. next_video_packet = next(filter(is_video, container_packets.peek())) - start_dts = next_video_packet.dts - next_video_packet.duration + # Since the is_valid filter has already been applied before the following + # adjustment, it does not filter out the case where the duration below is + # 0 and both the first_keyframe and next_video_packet end up with the same + # dts. Use "or 1" to deal with this. + start_dts = next_video_packet.dts - (next_video_packet.duration or 1) first_keyframe.dts = first_keyframe.pts = start_dts except (av.AVError, StopIteration) as ex: _LOGGER.error("Error demuxing stream while finding first packet: %s", str(ex)) From a75c7d52c984edf84332212d7245a221feaca8b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 02:00:52 -0700 Subject: [PATCH 051/199] Cost sensor handle consumption sensor in Wh (#53746) --- homeassistant/components/energy/sensor.py | 24 ++++++ tests/components/energy/test_sensor.py | 89 +++++++++++++++++++++-- 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1c42ea5a050..e974035cbd6 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from functools import partial +import logging from typing import Any, Final, Literal, TypeVar, cast from homeassistant.components.sensor import ( @@ -11,6 +12,11 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, +) from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -20,6 +26,8 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN from .data import EnergyManager, async_get_manager +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -188,6 +196,12 @@ class EnergyCostSensor(SensorEntity): energy_price = float(energy_price_state.state) except ValueError: return + + if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( + f"/{ENERGY_WATT_HOUR}" + ): + energy_price *= 1000.0 + else: energy_price_state = None energy_price = cast(float, self._flow["number_energy_price"]) @@ -197,6 +211,16 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state) return + energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if energy_unit == ENERGY_WATT_HOUR: + energy_price /= 1000 + elif energy_unit != ENERGY_KILO_WATT_HOUR: + _LOGGER.warning( + "Found unexpected unit %s for %s", energy_unit, energy_state.entity_id + ) + return + if ( energy_state.attributes[ATTR_LAST_RESET] != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index f3af93c06c1..978b21e1919 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -16,6 +16,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_MONETARY, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -133,7 +135,9 @@ async def test_cost_sensor_price_entity( # Optionally initialize dependent entities if initial_energy is not None: hass.states.async_set( - usage_sensor_entity_id, initial_energy, {"last_reset": last_reset} + usage_sensor_entity_id, + initial_energy, + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) hass.states.async_set("sensor.energy_price", "1") @@ -152,7 +156,12 @@ async def test_cost_sensor_price_entity( if initial_energy is None: with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( - usage_sensor_entity_id, "0", {"last_reset": last_reset} + usage_sensor_entity_id, + "0", + { + "last_reset": last_reset, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + }, ) await hass.async_block_till_done() @@ -169,7 +178,11 @@ async def test_cost_sensor_price_entity( # # assert entry.unique_id == "energy_energy_consumption cost" # Energy use bumped to 10 kWh - hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "10", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR @@ -189,7 +202,11 @@ async def test_cost_sensor_price_entity( assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR # Additional consumption is using the new price - hass.states.async_set(usage_sensor_entity_id, "14.5", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "14.5", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR @@ -202,13 +219,21 @@ async def test_cost_sensor_price_entity( # Energy sensor is reset, with start point at 4kWh last_reset = (now + timedelta(seconds=1)).isoformat() - hass.states.async_set(usage_sensor_entity_id, "4", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "4", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" # 0 EUR + (4-4) kWh * 2 EUR/kWh = 0 EUR # Energy use bumped to 10 kWh - hass.states.async_set(usage_sensor_entity_id, "10", {"last_reset": last_reset}) + hass.states.async_set( + usage_sensor_entity_id, + "10", + {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) assert state.state == "12.0" # 0 EUR + (10-4) kWh * 2 EUR/kWh = 12 EUR @@ -218,3 +243,55 @@ async def test_cost_sensor_price_entity( statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) assert cost_sensor_entity_id in statistics assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 31.0 + + +async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: + """Test energy cost price from sensor entity.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "entity_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 0.5, + } + ], + "flow_to": [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + now = dt_util.utcnow() + last_reset = dt_util.utc_from_timestamp(0).isoformat() + + hass.states.async_set( + "sensor.energy_consumption", + 10000, + {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + ) + + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass) + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "0.0" + + # Energy use bumped to 10 kWh + hass.states.async_set( + "sensor.energy_consumption", + 20000, + {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "5.0" From fc8af9af8ee38aa3948cd69ad10292da219e8ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 30 Jul 2021 21:44:10 +0200 Subject: [PATCH 052/199] Revert "Rename snapshot -> backup" (#53751) This reverts commit 9806bda272bd855f27a2ebdda97e1725474ad03e. --- homeassistant/components/hassio/__init__.py | 57 +++++------------- homeassistant/components/hassio/http.py | 9 +-- homeassistant/components/hassio/services.yaml | 58 ++----------------- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/addon.py | 14 ++--- tests/components/hassio/test_http.py | 20 +++---- tests/components/hassio/test_init.py | 43 ++++---------- tests/components/hassio/test_websocket_api.py | 4 +- tests/components/zwave_js/conftest.py | 12 ++-- tests/components/zwave_js/test_init.py | 44 +++++++------- 10 files changed, 82 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 4541685061a..6c71f2eb042 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -89,8 +89,6 @@ SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" SERVICE_SNAPSHOT_FULL = "snapshot_full" SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" -SERVICE_BACKUP_FULL = "backup_full" -SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" @@ -103,11 +101,11 @@ SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} ) -SCHEMA_BACKUP_FULL = vol.Schema( +SCHEMA_SNAPSHOT_FULL = vol.Schema( {vol.Optional(ATTR_NAME): cv.string, vol.Optional(ATTR_PASSWORD): cv.string} ) -SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( +SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend( { vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), @@ -115,12 +113,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( ) SCHEMA_RESTORE_FULL = vol.Schema( - { - vol.Exclusive(ATTR_SLUG, ATTR_SLUG): cv.slug, - vol.Exclusive(ATTR_SNAPSHOT, ATTR_SLUG): cv.slug, - vol.Optional(ATTR_PASSWORD): cv.string, - }, - cv.has_at_least_one_key(ATTR_SLUG, ATTR_SNAPSHOT), + {vol.Required(ATTR_SNAPSHOT): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string} ) SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( @@ -140,32 +133,25 @@ MAP_SERVICE_API = { SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False), SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False), SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False), - SERVICE_BACKUP_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), - SERVICE_BACKUP_PARTIAL: ( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, + SERVICE_SNAPSHOT_FULL: ("/snapshots/new/full", SCHEMA_SNAPSHOT_FULL, 300, True), + SERVICE_SNAPSHOT_PARTIAL: ( + "/snapshots/new/partial", + SCHEMA_SNAPSHOT_PARTIAL, 300, True, ), SERVICE_RESTORE_FULL: ( - "/backups/{slug}/restore/full", + "/snapshots/{snapshot}/restore/full", SCHEMA_RESTORE_FULL, 300, True, ), SERVICE_RESTORE_PARTIAL: ( - "/backups/{slug}/restore/partial", + "/snapshots/{snapshot}/restore/partial", SCHEMA_RESTORE_PARTIAL, 300, True, ), - SERVICE_SNAPSHOT_FULL: ("/backups/new/full", SCHEMA_BACKUP_FULL, 300, True), - SERVICE_SNAPSHOT_PARTIAL: ( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, - 300, - True, - ), } @@ -286,16 +272,16 @@ async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict @bind_hass @api_data -async def async_create_backup( +async def async_create_snapshot( hass: HomeAssistant, payload: dict, partial: bool = False ) -> dict: - """Create a full or partial backup. + """Create a full or partial snapshot. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] - backup_type = "partial" if partial else "full" - command = f"/backups/new/{backup_type}" + snapshot_type = "partial" if partial else "full" + command = f"/snapshots/new/{snapshot_type}" return await hassio.send_command(command, payload=payload, timeout=None) @@ -467,22 +453,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_service_handler(service): """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] - if "snapshot" in service.service: - _LOGGER.warning( - "The service '%s' is deprecated and will be removed in Home Assistant 2021.10, use '%s' instead", - service.service, - service.service.replace("snapshot", "backup"), - ) data = service.data.copy() addon = data.pop(ATTR_ADDON, None) - slug = data.pop(ATTR_SLUG, None) snapshot = data.pop(ATTR_SNAPSHOT, None) - if snapshot is not None: - _LOGGER.warning( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" - ) - slug = snapshot - payload = None # Pass data to Hass.io API @@ -494,12 +467,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Call API try: await hassio.send_command( - api_command.format(addon=addon, slug=slug), + api_command.format(addon=addon, snapshot=snapshot), payload=payload, timeout=MAP_SERVICE_API[service.service][2], ) except HassioAPIError as err: - _LOGGER.error("Error on Supervisor API: %s", err) + _LOGGER.error("Error on Hass.io API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 302cc00bb9f..47131b80de3 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -29,9 +29,6 @@ NO_TIMEOUT = re.compile( r"|hassos/update/cli" r"|supervisor/update" r"|addons/[^/]+/(?:update|install|rebuild)" - r"|backups/.+/full" - r"|backups/.+/partial" - r"|backups/[^/]+/(?:upload|download)" r"|snapshots/.+/full" r"|snapshots/.+/partial" r"|snapshots/[^/]+/(?:upload|download)" @@ -39,7 +36,7 @@ NO_TIMEOUT = re.compile( ) NO_AUTH_ONBOARDING = re.compile( - r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r"|snapshots/[^/]+/.+" r")$" + r"^(?:" r"|supervisor/logs" r"|snapshots/[^/]+/.+" r")$" ) NO_AUTH = re.compile( @@ -84,13 +81,13 @@ class HassIOView(HomeAssistantView): client_timeout = 10 data = None headers = _init_header(request) - if path in ("snapshots/new/upload", "backups/new/upload"): + if path == "snapshots/new/upload": # We need to reuse the full content type that includes the boundary headers[ "Content-Type" ] = request._stored_content_type # pylint: disable=protected-access - # Backups are big, so we need to adjust the allowed size + # Snapshots are big, so we need to adjust the allowed size request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE ) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 38d78984ddc..0652b65d6e2 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -67,13 +67,13 @@ host_shutdown: description: Poweroff the host system. snapshot_full: - name: Create a full backup. - description: Create a full backup (deprecated, use backup_full instead). + name: Create a full snapshot. + description: Create a full snapshot. fields: name: name: Name description: Optional or it will be the current date and time. - example: "backup 1" + example: "Snapshot 1" selector: text: password: @@ -84,8 +84,8 @@ snapshot_full: text: snapshot_partial: - name: Create a partial backup. - description: Create a partial backup (deprecated, use backup_partial instead). + name: Create a partial snapshot. + description: Create a partial snapshot. fields: addons: name: Add-ons @@ -102,53 +102,7 @@ snapshot_partial: name: name: Name description: Optional or it will be the current date and time. - example: "Partial backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -backup_full: - name: Create a full backup. - description: Create a full backup. - fields: - name: - name: Name - description: Optional or it will be the current date and time. - example: "backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -backup_partial: - name: Create a partial backup. - description: Create a partial backup. - fields: - addons: - name: Add-ons - description: Optional list of addon slugs. - example: ["core_ssh", "core_samba", "core_mosquitto"] - selector: - object: - folders: - name: Folders - description: Optional list of directories. - example: ["homeassistant", "share"] - selector: - object: - name: - name: Name - description: Optional or it will be the current date and time. - example: "Partial backup 1" + example: "Partial Snapshot 1" selector: text: password: diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6320efddb60..6cd0104e298 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -547,7 +547,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) return try: - await addon_manager.async_create_backup() + await addon_manager.async_create_snapshot() except AddonError as err: LOGGER.error(err) return diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 29ae887b4bc..a0caaa15488 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -8,7 +8,7 @@ from functools import partial from typing import Any, Callable, TypeVar, cast from homeassistant.components.hassio import ( - async_create_backup, + async_create_snapshot, async_get_addon_discovery_info, async_get_addon_info, async_install_addon, @@ -202,7 +202,7 @@ class AddonManager: if not addon_info.update_available: return - await self.async_create_backup() + await self.async_create_snapshot() await async_update_addon(self._hass, ADDON_SLUG) @callback @@ -289,14 +289,14 @@ class AddonManager: ) return self._start_task - @api_error("Failed to create a backup of the Z-Wave JS add-on.") - async def async_create_backup(self) -> None: - """Create a partial backup of the Z-Wave JS add-on.""" + @api_error("Failed to create a snapshot of the Z-Wave JS add-on.") + async def async_create_snapshot(self) -> None: + """Create a partial snapshot of the Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() name = f"addon_{ADDON_SLUG}_{addon_info.version}" - LOGGER.debug("Creating backup: %s", name) - await async_create_backup( + LOGGER.debug("Creating snapshot: %s", name) + await async_create_snapshot( self._hass, {"name": name, "addons": [ADDON_SLUG]}, partial=True, diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index fc4bb3e6a0d..ff1c348a37b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -132,13 +132,13 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo assert req_headers["X-Hass-Is-Admin"] == "1" -async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): - """Test that we forward the full header for backup upload.""" +async def test_snapshot_upload_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for snapshot upload.""" content_type = "multipart/form-data; boundary='--webkit'" - aioclient_mock.get("http://127.0.0.1/backups/new/upload") + aioclient_mock.get("http://127.0.0.1/snapshots/new/upload") resp = await hassio_client.get( - "/api/hassio/backups/new/upload", headers={"Content-Type": content_type} + "/api/hassio/snapshots/new/upload", headers={"Content-Type": content_type} ) # Check we got right response @@ -150,18 +150,18 @@ async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): assert req_headers["Content-Type"] == content_type -async def test_backup_download_headers(hassio_client, aioclient_mock): - """Test that we forward the full header for backup download.""" +async def test_snapshot_download_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for snapshot download.""" content_disposition = "attachment; filename=test.tar" aioclient_mock.get( - "http://127.0.0.1/backups/slug/download", + "http://127.0.0.1/snapshots/slug/download", headers={ "Content-Length": "50000000", "Content-Disposition": content_disposition, }, ) - resp = await hassio_client.get("/api/hassio/backups/slug/download") + resp = await hassio_client.get("/api/hassio/snapshots/slug/download") # Check we got right response assert resp.status == 200 @@ -174,9 +174,9 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") - assert _need_auth(hass, "backups/new/upload") + assert _need_auth(hass, "snapshots/new/upload") assert _need_auth(hass, "supervisor/logs") hass.data["onboarding"] = False - assert not _need_auth(hass, "backups/new/upload") + assert not _need_auth(hass, "snapshots/new/upload") assert not _need_auth(hass, "supervisor/logs") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 910ed12cb52..8377e5287d0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -303,13 +303,11 @@ async def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "host_reboot") assert hass.services.has_service("hassio", "snapshot_full") assert hass.services.has_service("hassio", "snapshot_partial") - assert hass.services.has_service("hassio", "backup_full") - assert hass.services.has_service("hassio", "backup_partial") assert hass.services.has_service("hassio", "restore_full") assert hass.services.has_service("hassio", "restore_partial") -async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): +async def test_service_calls(hassio_env, hass, aioclient_mock): """Call service and check the API calls behind that.""" assert await async_setup_component(hass, "hassio", {}) @@ -320,13 +318,13 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/backups/new/full", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/backups/new/partial", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/snapshots/new/full", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/snapshots/new/partial", json={"result": "ok"}) aioclient_mock.post( - "http://127.0.0.1/backups/test/restore/full", json={"result": "ok"} + "http://127.0.0.1/snapshots/test/restore/full", json={"result": "ok"} ) aioclient_mock.post( - "http://127.0.0.1/backups/test/restore/partial", json={"result": "ok"} + "http://127.0.0.1/snapshots/test/restore/partial", json={"result": "ok"} ) await hass.services.async_call("hassio", "addon_start", {"addon": "test"}) @@ -347,48 +345,27 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): assert aioclient_mock.call_count == 10 - await hass.services.async_call("hassio", "backup_full", {}) - await hass.services.async_call( - "hassio", - "backup_partial", - {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, - ) await hass.services.async_call("hassio", "snapshot_full", {}) await hass.services.async_call( "hassio", "snapshot_partial", - {"addons": ["test"], "folders": ["ssl"]}, + {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, ) await hass.async_block_till_done() - assert ( - "The service 'snapshot_full' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_full' instead" - in caplog.text - ) - assert ( - "The service 'snapshot_partial' is deprecated and will be removed in Home Assistant 2021.10, use 'backup_partial' instead" - in caplog.text - ) - assert aioclient_mock.call_count == 14 - assert aioclient_mock.mock_calls[-3][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], "password": "123456", } - await hass.services.async_call("hassio", "restore_full", {"slug": "test"}) await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) - await hass.async_block_till_done() - assert ( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.10, use 'slug' instead" - in caplog.text - ) - await hass.services.async_call( "hassio", "restore_partial", { - "slug": "test", + "snapshot": "test", "homeassistant": False, "addons": ["test"], "folders": ["ssl"], @@ -397,7 +374,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 17 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5578194b87c..5278d2cbb91 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,7 +61,7 @@ async def test_websocket_supervisor_api( assert await async_setup_component(hass, "hassio", {}) websocket_client = await hass_ws_client(hass) aioclient_mock.post( - "http://127.0.0.1/backups/new/partial", + "http://127.0.0.1/snapshots/new/partial", json={"result": "ok", "data": {"slug": "sn_slug"}}, ) @@ -69,7 +69,7 @@ async def test_websocket_supervisor_api( { WS_ID: 1, WS_TYPE: WS_TYPE_API, - ATTR_ENDPOINT: "/backups/new/partial", + ATTR_ENDPOINT: "/snapshots/new/partial", ATTR_METHOD: "post", } ) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 75b5ab65d38..0f336e396fe 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -171,13 +171,13 @@ def uninstall_addon_fixture(): yield uninstall_addon -@pytest.fixture(name="create_backup") -def create_backup_fixture(): - """Mock create backup.""" +@pytest.fixture(name="create_shapshot") +def create_snapshot_fixture(): + """Mock create snapshot.""" with patch( - "homeassistant.components.zwave_js.addon.async_create_backup" - ) as create_backup: - yield create_backup + "homeassistant.components.zwave_js.addon.async_create_snapshot" + ) as create_shapshot: + yield create_shapshot @pytest.fixture(name="controller_state", scope="session") diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 447b052b8c0..0b9009cd1d7 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -365,8 +365,8 @@ async def test_addon_options_changed( @pytest.mark.parametrize( - "addon_version, update_available, update_calls, backup_calls, " - "update_addon_side_effect, create_backup_side_effect", + "addon_version, update_available, update_calls, snapshot_calls, " + "update_addon_side_effect, create_shapshot_side_effect", [ ("1.0", True, 1, 1, None, None), ("1.0", False, 0, 0, None, None), @@ -380,15 +380,15 @@ async def test_update_addon( addon_info, addon_installed, addon_running, - create_backup, + create_shapshot, update_addon, addon_options, addon_version, update_available, update_calls, - backup_calls, + snapshot_calls, update_addon_side_effect, - create_backup_side_effect, + create_shapshot_side_effect, ): """Test update the Z-Wave JS add-on during entry setup.""" device = "/test" @@ -397,7 +397,7 @@ async def test_update_addon( addon_options["network_key"] = network_key addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available - create_backup.side_effect = create_backup_side_effect + create_shapshot.side_effect = create_shapshot_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion("Invalid version") entry = MockConfigEntry( @@ -416,7 +416,7 @@ async def test_update_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert create_backup.call_count == backup_calls + assert create_shapshot.call_count == snapshot_calls assert update_addon.call_count == update_calls @@ -469,7 +469,7 @@ async def test_stop_addon( async def test_remove_entry( - hass, addon_installed, stop_addon, create_backup, uninstall_addon, caplog + hass, addon_installed, stop_addon, create_shapshot, uninstall_addon, caplog ): """Test remove the config entry.""" # test successful remove without created add-on @@ -500,8 +500,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -511,7 +511,7 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on stop failure @@ -523,27 +523,27 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 0 + assert create_shapshot.call_count == 0 assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() - # test create backup failure + # test create snapshot failure entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - create_backup.side_effect = HassioAPIError() + create_shapshot.side_effect = HassioAPIError() await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, @@ -551,10 +551,10 @@ async def test_remove_entry( assert uninstall_addon.call_count == 0 assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to create a backup of the Z-Wave JS add-on" in caplog.text - create_backup.side_effect = None + assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text + create_shapshot.side_effect = None stop_addon.reset_mock() - create_backup.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on uninstall failure @@ -566,8 +566,8 @@ async def test_remove_entry( assert stop_addon.call_count == 1 assert stop_addon.call_args == call(hass, "core_zwave_js") - assert create_backup.call_count == 1 - assert create_backup.call_args == call( + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( hass, {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, partial=True, From 423fb910b588239a86ad2d4d0c6a0dd9e40ebaf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jul 2021 14:44:28 -0500 Subject: [PATCH 053/199] Bump HAP-python to 3.6.0 (#53754) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_cameras.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 5ec935611f7..887dfc3ee37 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.5.2", + "HAP-python==3.6.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 75313a53947..1e469e39839 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.5.2 +HAP-python==3.6.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a49e3faae2..b0c2223e7d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.5.2 +HAP-python==3.6.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 354db900470..b9df572a699 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -696,21 +696,21 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char - assert char.value == 0 + assert char.value is None service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH) assert service2 char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char2 - assert char2.value == 0 + assert char2.value is None hass.states.async_set( doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None char.set_value(True) char2.set_value(True) @@ -718,8 +718,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None # Ensure we do not throw when the linked # doorbell sensor is removed @@ -727,8 +727,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() - assert char.value == 0 - assert char2.value == 0 + assert char.value is None + assert char2.value is None async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, events): From 0aee659ee94990b11d4df62c352c86663abc91bb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 30 Jul 2021 21:44:52 +0200 Subject: [PATCH 054/199] Fix Xiaomi Miio humidifier mode change (#53757) --- homeassistant/components/xiaomi_miio/humidifier.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index eb45a716254..64248f900e0 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -232,7 +232,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): """Return the target humidity.""" return ( self._target_humidity - if self._mode == AirhumidifierOperationMode.Auto.name + if self._mode == AirhumidifierOperationMode.Auto.value or AirhumidifierOperationMode.Auto.name not in self.available_modes else None ) @@ -264,7 +264,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): self._device.set_mode, AirhumidifierOperationMode.Auto, ): - self._mode = AirhumidifierOperationMode.Auto.name + self._mode = AirhumidifierOperationMode.Auto.value self.async_write_ha_state() async def async_set_mode(self, mode: str) -> None: @@ -280,9 +280,9 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - AirhumidifierOperationMode[mode.title()], + AirhumidifierOperationMode[mode], ): - self._mode = mode.title() + self._mode = mode.lower() self.async_write_ha_state() From 958df580a9b73de90c791443aa2745c2565a6edf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Jul 2021 21:47:55 +0200 Subject: [PATCH 055/199] Fix Xiaomi-miio humidifier write the state back when turning on or off (#53771) --- homeassistant/components/xiaomi_miio/humidifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 64248f900e0..aee2c237066 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -139,6 +139,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): ) if result: self._state = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" @@ -148,6 +149,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): if result: self._state = False + self.async_write_ha_state() def translate_humidity(self, humidity): """Translate the target humidity to the first valid step.""" From 9f0f40dac6036a6ff0661dd91494f79807a83bd9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Jul 2021 15:13:53 -0600 Subject: [PATCH 056/199] Fix parsing of non-string values in Slack data (#53775) --- homeassistant/components/slack/notify.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 0eadc26075e..4dfacda266c 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -146,20 +146,6 @@ def _async_sanitize_channel_names(channel_list: list[str]) -> list[str]: return [channel.lstrip("#") for channel in channel_list] -@callback -def _async_templatize_blocks(hass: HomeAssistant, value: Any) -> Any: - """Recursive template creator helper function.""" - if isinstance(value, list): - return [_async_templatize_blocks(hass, item) for item in value] - if isinstance(value, dict): - return { - key: _async_templatize_blocks(hass, item) for key, item in value.items() - } - - tmpl = template.Template(value, hass=hass) # type: ignore # no-untyped-call - return tmpl.async_render(parse_result=False) - - class SlackNotificationService(BaseNotificationService): """Define the Slack notification logic.""" @@ -314,9 +300,9 @@ class SlackNotificationService(BaseNotificationService): # Message Type 1: A text-only message if ATTR_FILE not in data: if ATTR_BLOCKS_TEMPLATE in data: - blocks = _async_templatize_blocks( - self._hass, data[ATTR_BLOCKS_TEMPLATE] - ) + value = cv.template_complex(data[ATTR_BLOCKS_TEMPLATE]) + template.attach(self._hass, value) + blocks = template.render_complex(value) elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: From af96c5d60c0bbfb0a90d69908c205b6b1cbe808d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Jul 2021 23:11:47 +0200 Subject: [PATCH 057/199] Update frontend to 20210730.0 (#53778) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ac791977038..02ee134523e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210729.0" + "home-assistant-frontend==20210730.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e1acf1dba15..1418f59fedf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1e469e39839..517e71c9b73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0c2223e7d2..03412d8e729 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210729.0 +home-assistant-frontend==20210730.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From cd3390e01273f8692997d74bf00ddccf40a733fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 14:14:58 -0700 Subject: [PATCH 058/199] Bump Hue and only fire events for button presses (#53781) * Bump Hue and only fire events for button presses * Fix tests --- homeassistant/components/hue/hue_event.py | 12 +++++++++++- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_sensor_base.py | 3 +++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index 7c0163f8a16..6bd68b106bb 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -1,7 +1,12 @@ """Representation of a Hue remote firing events for button presses.""" import logging -from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH +from aiohue.sensors import ( + EVENT_BUTTON, + TYPE_ZGP_SWITCH, + TYPE_ZLL_ROTARY, + TYPE_ZLL_SWITCH, +) from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback @@ -50,6 +55,11 @@ class HueEvent(GenericHueDevice): """Fire the event if reason is that state is updated.""" if ( self.sensor.state == self._last_state + # Filter out non-button events if last event type is available + or ( + self.sensor.last_event is not None + and self.sensor.last_event["type"] != EVENT_BUTTON + ) or # Filter out old states. Can happen when events fire while refreshing dt_util.parse_datetime(self.sensor.state["lastupdated"]) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 3c8078364ab..05e69948218 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.5.1"], + "requirements": ["aiohue==2.6.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 517e71c9b73..a2a24953b71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.1 +aiohue==2.6.0 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03412d8e729..4c934e07675 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -122,7 +122,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.1 +aiohue==2.6.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index bc11c013555..b8e9c83e47d 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -446,6 +446,9 @@ async def test_hue_events(hass, mock_bridge): assert len(hass.states.async_all()) == 7 assert len(events) == 0 + mock_bridge.api.sensors["7"].last_event = {"type": "button"} + mock_bridge.api.sensors["8"].last_event = {"type": "button"} + new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"]["state"] = { "buttonevent": 18, From e0fc14f82c8f121fb11f494b98e4b72e3beb2ea0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jul 2021 14:15:42 -0700 Subject: [PATCH 059/199] Bumped version to 2021.8.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bc79728c307..9a3f47848bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2297c0b58b4e141be28aaebac77571fd66be4d32 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 1 Aug 2021 23:58:55 +0200 Subject: [PATCH 060/199] Do not block setup of TP-Link when device unreachable (#53770) --- homeassistant/components/tplink/__init__.py | 70 ++++++++++++++++---- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/consts.py | 29 +++++++- tests/components/tplink/test_init.py | 73 ++++++++++++++------- 4 files changed, 137 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index e309d2c5082..88160722669 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging import time +from typing import Any from pyHS100.smartdevice import SmartDevice, SmartDeviceException from pyHS100.smartplug import SmartPlug @@ -22,9 +23,9 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utc_from_timestamp @@ -44,6 +45,8 @@ from .const import ( CONF_SWITCH, COORDINATORS, PLATFORMS, + UNAVAILABLE_DEVICES, + UNAVAILABLE_RETRY_DELAY, ) _LOGGER = logging.getLogger(__name__) @@ -96,16 +99,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) + if config_data is None and entry.data: + config_data = entry.data + elif config_data is not None: + hass.config_entries.async_update_entry(entry, data=config_data) device_registry = dr.async_get(hass) tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) device_count = len(tplink_devices) + hass_data: dict[str, Any] = hass.data[DOMAIN] # These will contain the initialized devices - hass.data[DOMAIN][CONF_LIGHT] = [] - hass.data[DOMAIN][CONF_SWITCH] = [] - lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT] - switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH] + hass_data[CONF_LIGHT] = [] + hass_data[CONF_SWITCH] = [] + hass_data[UNAVAILABLE_DEVICES] = [] + lights: list[SmartDevice] = hass_data[CONF_LIGHT] + switches: list[SmartPlug] = hass_data[CONF_SWITCH] + unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES] # Add static devices static_devices = SmartDevices() @@ -136,22 +146,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ", ".join(d.host for d in switches), ) + async def async_retry_devices(self) -> None: + """Retry unavailable devices.""" + unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES] + _LOGGER.debug( + "retry during setup unavailable devices: %s", + [d.host for d in unavailable_devices], + ) + + for device in unavailable_devices: + try: + device.get_sysinfo() + except SmartDeviceException: + continue + _LOGGER.debug( + "at least one device is available again, so reload integration" + ) + await hass.config_entries.async_reload(entry.entry_id) + break + # prepare DataUpdateCoordinators - hass.data[DOMAIN][COORDINATORS] = {} + hass_data[COORDINATORS] = {} for switch in switches: try: await hass.async_add_executor_job(switch.get_sysinfo) - except SmartDeviceException as ex: - _LOGGER.debug(ex) - raise ConfigEntryNotReady from ex + except SmartDeviceException: + _LOGGER.warning( + "Device at '%s' not reachable during setup, will retry later", + switch.host, + ) + unavailable_devices.append(switch) + continue - hass.data[DOMAIN][COORDINATORS][ + hass_data[COORDINATORS][ switch.mac ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) await coordinator.async_config_entry_first_refresh() + if unavailable_devices: + entry.async_on_unload( + async_track_time_interval( + hass, async_retry_devices, UNAVAILABLE_RETRY_DELAY + ) + ) + unavailable_devices_hosts = [d.host for d in unavailable_devices] + hass_data[CONF_SWITCH] = [ + s for s in switches if s.host not in unavailable_devices_hosts + ] + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -159,10 +203,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - platforms = [platform for platform in PLATFORMS if hass.data[DOMAIN].get(platform)] - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + hass_data: dict[str, Any] = hass.data[DOMAIN] if unload_ok: - hass.data[DOMAIN].clear() + hass_data.clear() return unload_ok diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 888d671096d..60e06fd1ffe 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -5,6 +5,8 @@ import datetime DOMAIN = "tplink" COORDINATORS = "coordinators" +UNAVAILABLE_DEVICES = "unavailable_devices" +UNAVAILABLE_RETRY_DELAY = datetime.timedelta(seconds=300) MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) MAX_DISCOVERY_RETRIES = 4 diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py index de134ddbe07..95177a12a9c 100644 --- a/tests/components/tplink/consts.py +++ b/tests/components/tplink/consts.py @@ -1,6 +1,6 @@ """Constants for the TP-Link component tests.""" -SMARTPLUGSWITCH_DATA = { +SMARTPLUG_HS110_DATA = { "sysinfo": { "sw_ver": "1.0.4 Build 191111 Rel.143500", "hw_ver": "4.0", @@ -34,6 +34,33 @@ SMARTPLUGSWITCH_DATA = { "err_code": 0, }, } +SMARTPLUG_HS100_DATA = { + "sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS100(EU)", + "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", + "oemId": "40F54B43071E9436B6395611E9D91CEA", + "hwId": "A6C77E4FDD238B53D824AC8DA361F043", + "rssi": -24, + "longitude_i": 130793, + "latitude_i": 480582, + "alias": "SmartPlug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:", + "mac": "A9:F4:3D:A4:E3:47", + "updating": 0, + "led_off": 0, + "relay_state": 0, + "on_time": 0, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug", + "next_action": {"type": -1}, + "err_code": 0, + } +} SMARTSTRIPWITCH_DATA = { "sysinfo": { "sw_ver": "1.0.4 Build 191111 Rel.143500", diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index fb3f44709fc..a201788f35b 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -11,6 +11,8 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.common import SmartDevices from homeassistant.components.tplink.const import ( CONF_DIMMER, @@ -19,16 +21,21 @@ from homeassistant.components.tplink.const import ( CONF_SW_VERSION, CONF_SWITCH, COORDINATORS, + UNAVAILABLE_RETRY_DELAY, ) from homeassistant.components.tplink.sensor import ENERGY_SENSORS from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util import slugify +from homeassistant.util import dt, slugify -from tests.common import MockConfigEntry, mock_coro -from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA +from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro +from tests.components.tplink.consts import ( + SMARTPLUG_HS100_DATA, + SMARTPLUG_HS110_DATA, + SMARTSTRIPWITCH_DATA, +) async def test_creating_entry_tries_discover(hass): @@ -220,9 +227,9 @@ async def test_platforms_are_initialized(hass: HomeAssistant): light = SmartBulb("123.123.123.123") switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"]) switch.get_emeter_realtime = MagicMock( - return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"]) ) switch.get_emeter_daily = MagicMock( return_value={int(time.strftime("%e")): 1.123} @@ -270,25 +277,22 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant): ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ), patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), ), patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"]) get_static_devices.return_value = SmartDevices([], [switch]) await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - for description in ENERGY_SENSORS: - state = hass.states.get( - f"sensor.{switch.alias}_{slugify(description.name)}" - ) - assert state is None + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 1 + + entities = hass.states.async_entity_ids(SENSOR_DOMAIN) + assert len(entities) == 0 async def test_smartstrip_device(hass: HomeAssistant): @@ -346,8 +350,8 @@ async def test_no_config_creates_no_entry(hass): assert mock_setup.call_count == 0 -async def test_not_ready(hass: HomeAssistant): - """Test for not ready when configured devices are not available.""" +async def test_not_available_at_startup(hass: HomeAssistant): + """Test when configured devices are not available.""" config = { tplink.DOMAIN: { CONF_DISCOVERY: False, @@ -362,9 +366,6 @@ async def test_not_ready(hass: HomeAssistant): ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ), patch( - "homeassistant.components.tplink.switch.async_setup_entry", - return_value=mock_coro(True), ), patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): @@ -373,13 +374,39 @@ async def test_not_ready(hass: HomeAssistant): switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) get_static_devices.return_value = SmartDevices([], [switch]) + # run setup while device unreachable await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() entries = hass.config_entries.async_entries(tplink.DOMAIN) - assert len(entries) == 1 - assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 0 + + # retrying with still unreachable device + async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 0 + + # retrying with now reachable device + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"]) + async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(tplink.DOMAIN) + assert len(entries) == 1 + assert entries[0].state is config_entries.ConfigEntryState.LOADED + + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 1 @pytest.mark.parametrize("platform", ["switch", "light"]) @@ -406,9 +433,9 @@ async def test_unload(hass, platform): light = SmartBulb("123.123.123.123") switch = SmartPlug("321.321.321.321") - switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"]) + switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"]) switch.get_emeter_realtime = MagicMock( - return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"]) + return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"]) ) if platform == "light": get_static_devices.return_value = SmartDevices([light], []) From 31869cbb12ae36772c1df43d39e1088bf4c047a9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 31 Jul 2021 12:32:16 +0200 Subject: [PATCH 061/199] Fix name migration of the Xiaomi Miio humidifier (#53790) --- homeassistant/components/xiaomi_miio/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e858c7ee797..36ee89ba7a0 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -99,7 +99,6 @@ async def async_create_miio_device_and_coordinator( token = entry.data[CONF_TOKEN] name = entry.title device = None - migrate_entity_name = None if model not in MODELS_HUMIDIFIER: return @@ -116,8 +115,8 @@ async def async_create_miio_device_and_coordinator( entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future - migrate_entity_name = entity_registry.async_get(entity_id).name - hass.config_entries.async_update_entry(entry, title=migrate_entity_name) + if migrate_entity_name := entity_registry.async_get(entity_id).name: + hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) async def async_update_data(): From 0948eafb93c480beb820596bdf86cae3c63077e1 Mon Sep 17 00:00:00 2001 From: Andreas Brett Date: Sat, 31 Jul 2021 14:47:51 +0200 Subject: [PATCH 062/199] Fix onkyo UnboundLocalError (#53793) audio_information_raw and video_information_raw were in some cases used before being assigned error: UnboundLocalError: local variable 'video_information_raw' referenced before assignment --- homeassistant/components/onkyo/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 2e4b6eff6da..ef20c1054f3 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -319,8 +319,10 @@ class OnkyoDevice(MediaPlayerEntity): preset_raw = self.command("preset query") if self._audio_info_supported: audio_information_raw = self.command("audio-information query") + self._parse_audio_information(audio_information_raw) if self._video_info_supported: video_information_raw = self.command("video-information query") + self._parse_video_information(video_information_raw) if not (volume_raw and mute_raw and current_source_raw): return @@ -343,9 +345,6 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver_max_volume * self._max_volume / 100 ) - self._parse_audio_information(audio_information_raw) - self._parse_video_information(video_information_raw) - if not hdmi_out_raw: return self._attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) From fc5c30775dd2779c1051db3e6d1388cce1e463da Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 1 Aug 2021 23:58:31 +0200 Subject: [PATCH 063/199] Remove `led` from Xiaomi Miio humidifier features (#53796) --- homeassistant/components/xiaomi_miio/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 0d8d5bc0014..d250d8d4d2b 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -269,7 +269,6 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( FEATURE_FLAGS_AIRHUMIDIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_TARGET_HUMIDITY ) From fd0ae7ab360eb7e7182ace3389ee812a3b6f7198 Mon Sep 17 00:00:00 2001 From: B-Hartley Date: Sun, 1 Aug 2021 23:01:34 +0100 Subject: [PATCH 064/199] ForecastSolar - power production now w not k w (#53797) --- homeassistant/components/forecast_solar/const.py | 11 ++++------- tests/components/forecast_solar/test_sensor.py | 8 ++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 7f426d2847c..7ae6fe01d42 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -53,7 +53,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_now", name="Estimated Power Production - Now", device_class=DEVICE_CLASS_POWER, - state=lambda estimate: estimate.power_production_now / 1000, + state=lambda estimate: estimate.power_production_now, state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), @@ -61,8 +61,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_hour", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) - ) - / 1000, + ), name="Estimated Power Production - Next Hour", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -72,8 +71,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_12hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) - ) - / 1000, + ), name="Estimated Power Production - Next 12 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, @@ -83,8 +81,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="power_production_next_24hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) - ) - / 1000, + ), name="Estimated Power Production - Next 24 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 8b8c1cc933e..a2b105ccbd1 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -96,7 +96,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_production_now" - assert state.state == "300.0" + assert state.state == "300000" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" ) @@ -175,17 +175,17 @@ async def test_disabled_by_default( ( "power_production_next_12hours", "Estimated Power Production - Next 12 Hours", - "600.0", + "600000", ), ( "power_production_next_24hours", "Estimated Power Production - Next 24 Hours", - "700.0", + "700000", ), ( "power_production_next_hour", "Estimated Power Production - Next Hour", - "400.0", + "400000", ), ], ) From 3f7ddb4706c1ffaa482e06f5f2f37a223b16d480 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 31 Jul 2021 21:19:00 +0200 Subject: [PATCH 065/199] Clean Xiaomi Miio humidifier services (#53806) --- homeassistant/components/xiaomi_miio/const.py | 5 --- .../components/xiaomi_miio/services.yaml | 43 ------------------- 2 files changed, 48 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index d250d8d4d2b..a2f7679bf1b 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -147,8 +147,6 @@ MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" SERVICE_SET_BUZZER = "set_buzzer" -SERVICE_SET_CLEAN_ON = "set_clean_on" -SERVICE_SET_CLEAN_OFF = "set_clean_off" SERVICE_SET_CLEAN = "set_clean" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" SERVICE_SET_FAN_LED_OFF = "fan_set_led_off" @@ -167,9 +165,6 @@ SERVICE_SET_LEARN_MODE_OFF = "fan_set_learn_mode_off" SERVICE_SET_VOLUME = "fan_set_volume" SERVICE_RESET_FILTER = "fan_reset_filter" SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" -SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" -SERVICE_SET_DRY_ON = "fan_set_dry_on" -SERVICE_SET_DRY_OFF = "fan_set_dry_off" SERVICE_SET_DRY = "set_dry" SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index 90d31765307..4c153292d7e 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -211,49 +211,6 @@ fan_set_extra_features: min: 0 max: 1 -fan_set_target_humidity: - name: Fan set target humidity - description: Set the target humidity. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - humidity: - name: Humidity - description: Target humidity. - required: true - selector: - number: - min: 30 - max: 80 - step: 10 - unit_of_measurement: '%' - -fan_set_dry_on: - name: Fan set dry on - description: Turn the dry mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - -fan_set_dry_off: - name: Fan set dry off - description: Turn the dry mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - fan_set_motor_speed: name: Fan set motor speed description: Set the target motor speed. From cb2103d96cbeb9733db55e5c6834c12360d09a1e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 1 Aug 2021 04:08:39 -0400 Subject: [PATCH 066/199] Fix file path error in nfandroidtv (#53814) --- homeassistant/components/nfandroidtv/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index c2a42760aec..8cc1b0031f7 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -201,8 +201,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - with open(local_path, "rb") as path_handle: - return path_handle + return open(local_path, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") From ec35b920522a72a6a0bfc9d12809b30e2ca31a5f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Aug 2021 00:06:28 +0200 Subject: [PATCH 067/199] Update frontend to 20210801.0 (#53841) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 02ee134523e..84de9b92c97 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210730.0" + "home-assistant-frontend==20210801.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1418f59fedf..db6ffd1d071 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index a2a24953b71..107a538550c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c934e07675..ea0e298a96b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210730.0 +home-assistant-frontend==20210801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1019bb059dfe687af603931800b61a848057ddac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Aug 2021 15:07:34 -0700 Subject: [PATCH 068/199] Bumped version to 2021.8.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9a3f47848bf..974fcd01b7b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From e093e0bf10ca30e8f06f5bebc1cb61d06d4ec83b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 2 Aug 2021 03:40:04 +0000 Subject: [PATCH 069/199] [ci skip] Translation update --- .../components/abode/translations/es-419.json | 6 ++- .../accuweather/translations/es-419.json | 5 ++ .../components/adax/translations/cs.json | 19 +++++++ .../components/adax/translations/fr.json | 6 +++ .../components/adax/translations/it.json | 20 ++++++++ .../components/aemet/translations/de.json | 2 +- .../components/aemet/translations/fr.json | 9 ++++ .../airvisual/translations/sensor.fr.json | 13 ++++- .../airvisual/translations/sensor.he.json | 8 +++ .../airvisual/translations/sensor.it.json | 20 ++++++++ .../airvisual/translations/sensor.no.json | 8 +++ .../alarm_control_panel/translations/fr.json | 2 + .../components/ambee/translations/fr.json | 28 +++++++++++ .../ambee/translations/sensor.fr.json | 10 ++++ .../ambee/translations/sensor.he.json | 9 ++++ .../components/arcam_fmj/translations/he.json | 8 ++- .../components/asuswrt/translations/de.json | 2 +- .../components/august/translations/he.json | 2 +- .../components/auth/translations/he.json | 6 +-- .../binary_sensor/translations/he.json | 24 ++++----- .../components/bosch_shc/translations/fr.json | 38 ++++++++++++++ .../components/brother/translations/de.json | 2 +- .../buienradar/translations/fr.json | 6 +++ .../components/calendar/translations/he.json | 2 +- .../components/cast/translations/he.json | 2 +- .../components/climacell/translations/de.json | 2 +- .../cloudflare/translations/cs.json | 6 +++ .../cloudflare/translations/fr.json | 7 +++ .../components/co2signal/translations/cs.json | 30 +++++++++++ .../components/co2signal/translations/fr.json | 8 ++- .../components/co2signal/translations/it.json | 34 +++++++++++++ .../components/coinbase/translations/cs.json | 19 +++++++ .../components/coinbase/translations/de.json | 2 +- .../components/coinbase/translations/fr.json | 41 +++++++++++++++ .../components/coinbase/translations/nl.json | 2 +- .../configurator/translations/he.json | 6 +-- .../conversation/translations/he.json | 2 +- .../coronavirus/translations/fr.json | 3 +- .../components/cover/translations/he.json | 4 +- .../demo/translations/select.fr.json | 9 ++++ .../device_tracker/translations/he.json | 2 +- .../devolo_home_control/translations/cs.json | 3 +- .../devolo_home_control/translations/fr.json | 6 ++- .../devolo_home_control/translations/he.json | 2 +- .../components/directv/translations/he.json | 8 +++ .../components/dsmr/translations/fr.json | 25 +++++++++- .../components/emonitor/translations/he.json | 2 +- .../components/energy/translations/cs.json | 3 ++ .../components/energy/translations/de.json | 3 ++ .../components/energy/translations/fr.json | 3 ++ .../components/energy/translations/he.json | 3 ++ .../components/energy/translations/it.json | 3 ++ .../components/energy/translations/nl.json | 3 ++ .../components/energy/translations/pl.json | 3 ++ .../components/energy/translations/ru.json | 3 ++ .../energy/translations/zh-Hant.json | 3 ++ .../enphase_envoy/translations/fr.json | 3 +- .../components/flipr/translations/cs.json | 20 ++++++++ .../components/flipr/translations/fr.json | 1 + .../components/flipr/translations/he.json | 5 ++ .../components/flipr/translations/it.json | 30 +++++++++++ .../components/flipr/translations/pl.json | 4 +- .../components/flume/translations/fr.json | 3 +- .../forecast_solar/translations/fr.json | 22 +++++++- .../freedompro/translations/fr.json | 11 ++++ .../components/fritz/translations/fr.json | 25 +++++++++- .../garages_amsterdam/translations/fr.json | 18 +++++++ .../components/goalzero/translations/fr.json | 8 ++- .../components/gogogate2/translations/fr.json | 1 + .../components/group/translations/he.json | 4 +- .../growatt_server/translations/cs.json | 11 ++++ .../growatt_server/translations/fr.json | 28 +++++++++++ .../growatt_server/translations/he.json | 5 ++ .../growatt_server/translations/it.json | 1 + .../components/guardian/translations/fr.json | 3 ++ .../components/harmony/translations/he.json | 2 +- .../home_plus_control/translations/de.json | 2 +- .../components/homekit/translations/it.json | 2 +- .../homekit_controller/translations/fr.json | 2 + .../components/honeywell/translations/cs.json | 15 ++++++ .../components/honeywell/translations/fr.json | 10 +++- .../components/honeywell/translations/it.json | 17 +++++++ .../huawei_lte/translations/fr.json | 3 +- .../components/hue/translations/fr.json | 4 +- .../translations/fr.json | 1 + .../translations/he.json | 2 +- .../input_boolean/translations/he.json | 2 +- .../components/isy994/translations/fr.json | 8 +++ .../keenetic_ndms2/translations/fr.json | 5 +- .../components/kodi/translations/he.json | 2 +- .../components/konnected/translations/he.json | 16 ++++++ .../components/kraken/translations/fr.json | 30 +++++++++++ .../components/kraken/translations/he.json | 21 ++++++++ .../components/litejet/translations/fr.json | 10 ++++ .../components/litejet/translations/it.json | 10 ++++ .../components/lyric/translations/fr.json | 6 ++- .../meteoclimatic/translations/fr.json | 20 ++++++++ .../components/mikrotik/translations/de.json | 6 +-- .../modern_forms/translations/fr.json | 28 +++++++++++ .../components/motioneye/translations/fr.json | 15 ++++-- .../components/myq/translations/fr.json | 3 +- .../components/mysensors/translations/de.json | 2 +- .../components/nam/translations/fr.json | 4 ++ .../components/nest/translations/he.json | 2 +- .../components/nexia/translations/fr.json | 1 + .../nfandroidtv/translations/cs.json | 19 +++++++ .../nfandroidtv/translations/fr.json | 21 ++++++++ .../nfandroidtv/translations/it.json | 21 ++++++++ .../nfandroidtv/translations/nl.json | 1 + .../nfandroidtv/translations/pl.json | 2 +- .../nmap_tracker/translations/cs.json | 7 +++ .../nmap_tracker/translations/fr.json | 32 +++++++++++- .../components/onvif/translations/fr.json | 13 +++++ .../components/onvif/translations/he.json | 2 +- .../ovo_energy/translations/he.json | 3 +- .../philips_js/translations/fr.json | 9 ++++ .../components/picnic/translations/fr.json | 5 ++ .../components/plugwise/translations/he.json | 3 ++ .../components/poolsense/translations/de.json | 2 +- .../components/prosegur/translations/ca.json | 29 +++++++++++ .../components/prosegur/translations/cs.json | 27 ++++++++++ .../components/prosegur/translations/de.json | 29 +++++++++++ .../components/prosegur/translations/et.json | 29 +++++++++++ .../components/prosegur/translations/fr.json | 29 +++++++++++ .../components/prosegur/translations/he.json | 28 +++++++++++ .../components/prosegur/translations/it.json | 29 +++++++++++ .../components/prosegur/translations/nl.json | 29 +++++++++++ .../components/prosegur/translations/no.json | 18 +++++++ .../components/prosegur/translations/pl.json | 29 +++++++++++ .../components/prosegur/translations/ru.json | 29 +++++++++++ .../prosegur/translations/zh-Hant.json | 29 +++++++++++ .../components/ps4/translations/he.json | 4 +- .../pvpc_hourly_pricing/translations/de.json | 2 +- .../pvpc_hourly_pricing/translations/fr.json | 15 ++++++ .../pvpc_hourly_pricing/translations/he.json | 7 +++ .../components/renault/translations/ca.json | 27 ++++++++++ .../components/renault/translations/cs.json | 18 +++++++ .../components/renault/translations/de.json | 27 ++++++++++ .../components/renault/translations/en.json | 44 ++++++++-------- .../components/renault/translations/et.json | 27 ++++++++++ .../components/renault/translations/fr.json | 27 ++++++++++ .../components/renault/translations/he.json | 18 +++++++ .../components/renault/translations/it.json | 27 ++++++++++ .../components/renault/translations/nl.json | 27 ++++++++++ .../components/renault/translations/no.json | 12 +++++ .../components/renault/translations/pl.json | 27 ++++++++++ .../components/renault/translations/ru.json | 27 ++++++++++ .../renault/translations/zh-Hant.json | 27 ++++++++++ .../components/roku/translations/he.json | 8 +++ .../components/roomba/translations/he.json | 2 +- .../components/roon/translations/nl.json | 2 +- .../components/samsungtv/translations/fr.json | 11 +++- .../screenlogic/translations/de.json | 2 +- .../components/select/translations/fr.json | 11 ++++ .../components/sia/translations/fr.json | 50 +++++++++++++++++++ .../simplisafe/translations/de.json | 2 +- .../components/sma/translations/fr.json | 12 +++-- .../smartthings/translations/he.json | 2 +- .../components/smarttub/translations/fr.json | 3 +- .../somfy_mylink/translations/de.json | 2 +- .../components/sonos/translations/fr.json | 1 + .../components/sonos/translations/he.json | 4 +- .../switcher_kis/translations/fr.json | 3 +- .../switcher_kis/translations/it.json | 13 +++++ .../components/syncthing/translations/fr.json | 22 ++++++++ .../synology_dsm/translations/cs.json | 10 +++- .../synology_dsm/translations/fr.json | 6 ++- .../synology_dsm/translations/he.json | 11 +++- .../synology_dsm/translations/it.json | 11 +++- .../system_bridge/translations/fr.json | 9 +++- .../system_health/translations/he.json | 2 +- .../components/tesla/translations/ca.json | 1 + .../components/tesla/translations/de.json | 1 + .../components/tesla/translations/et.json | 1 + .../components/tesla/translations/fr.json | 1 + .../components/tesla/translations/it.json | 1 + .../components/tesla/translations/nl.json | 1 + .../components/tesla/translations/pl.json | 1 + .../components/tesla/translations/ru.json | 1 + .../tesla/translations/zh-Hant.json | 1 + .../totalconnect/translations/fr.json | 3 +- .../components/tplink/translations/he.json | 4 +- .../components/traccar/translations/de.json | 2 +- .../components/unifi/translations/he.json | 8 +++ .../components/upb/translations/de.json | 2 +- .../components/upnp/translations/fr.json | 9 ++++ .../components/upnp/translations/he.json | 6 +++ .../components/wallbox/translations/fr.json | 22 ++++++++ .../components/wemo/translations/fr.json | 5 ++ .../components/wled/translations/fr.json | 9 ++++ .../wolflink/translations/sensor.de.json | 2 +- .../wolflink/translations/sensor.he.json | 1 + .../xiaomi_miio/translations/de.json | 2 +- .../xiaomi_miio/translations/fr.json | 36 +++++++++++-- .../xiaomi_miio/translations/he.json | 2 +- .../yale_smart_alarm/translations/ca.json | 8 +-- .../yale_smart_alarm/translations/cs.json | 28 +++++++++++ .../yale_smart_alarm/translations/en.json | 8 +-- .../yale_smart_alarm/translations/fr.json | 28 +++++++++++ .../yale_smart_alarm/translations/he.json | 28 +++++++++++ .../yale_smart_alarm/translations/it.json | 28 +++++++++++ .../yale_smart_alarm/translations/nl.json | 28 +++++++++++ .../yale_smart_alarm/translations/no.json | 20 ++++++++ .../yale_smart_alarm/translations/pl.json | 28 +++++++++++ .../yale_smart_alarm/translations/ru.json | 8 +-- .../translations/zh-Hant.json | 8 +-- .../yamaha_musiccast/translations/fr.json | 23 +++++++++ .../components/yeelight/translations/fr.json | 4 ++ .../components/youless/translations/ca.json | 15 ++++++ .../components/youless/translations/cs.json | 15 ++++++ .../components/youless/translations/de.json | 15 ++++++ .../components/youless/translations/en.json | 12 ++--- .../components/youless/translations/et.json | 15 ++++++ .../components/youless/translations/fr.json | 15 ++++++ .../components/youless/translations/he.json | 15 ++++++ .../components/youless/translations/it.json | 15 ++++++ .../components/youless/translations/nl.json | 15 ++++++ .../components/youless/translations/no.json | 11 ++++ .../components/youless/translations/pl.json | 15 ++++++ .../components/youless/translations/ru.json | 15 ++++++ .../youless/translations/zh-Hant.json | 15 ++++++ .../components/zha/translations/fr.json | 2 + .../components/zwave/translations/de.json | 2 +- .../components/zwave/translations/he.json | 6 +-- .../components/zwave_js/translations/de.json | 2 +- .../components/zwave_js/translations/fr.json | 48 +++++++++++++++++- 226 files changed, 2440 insertions(+), 174 deletions(-) create mode 100644 homeassistant/components/adax/translations/cs.json create mode 100644 homeassistant/components/adax/translations/it.json create mode 100644 homeassistant/components/airvisual/translations/sensor.he.json create mode 100644 homeassistant/components/airvisual/translations/sensor.it.json create mode 100644 homeassistant/components/airvisual/translations/sensor.no.json create mode 100644 homeassistant/components/ambee/translations/fr.json create mode 100644 homeassistant/components/ambee/translations/sensor.fr.json create mode 100644 homeassistant/components/ambee/translations/sensor.he.json create mode 100644 homeassistant/components/bosch_shc/translations/fr.json create mode 100644 homeassistant/components/co2signal/translations/cs.json create mode 100644 homeassistant/components/co2signal/translations/it.json create mode 100644 homeassistant/components/coinbase/translations/cs.json create mode 100644 homeassistant/components/coinbase/translations/fr.json create mode 100644 homeassistant/components/demo/translations/select.fr.json create mode 100644 homeassistant/components/energy/translations/cs.json create mode 100644 homeassistant/components/energy/translations/de.json create mode 100644 homeassistant/components/energy/translations/fr.json create mode 100644 homeassistant/components/energy/translations/he.json create mode 100644 homeassistant/components/energy/translations/it.json create mode 100644 homeassistant/components/energy/translations/nl.json create mode 100644 homeassistant/components/energy/translations/pl.json create mode 100644 homeassistant/components/energy/translations/ru.json create mode 100644 homeassistant/components/energy/translations/zh-Hant.json create mode 100644 homeassistant/components/flipr/translations/cs.json create mode 100644 homeassistant/components/flipr/translations/it.json create mode 100644 homeassistant/components/garages_amsterdam/translations/fr.json create mode 100644 homeassistant/components/growatt_server/translations/cs.json create mode 100644 homeassistant/components/growatt_server/translations/fr.json create mode 100644 homeassistant/components/honeywell/translations/cs.json create mode 100644 homeassistant/components/honeywell/translations/it.json create mode 100644 homeassistant/components/kraken/translations/fr.json create mode 100644 homeassistant/components/meteoclimatic/translations/fr.json create mode 100644 homeassistant/components/modern_forms/translations/fr.json create mode 100644 homeassistant/components/nfandroidtv/translations/cs.json create mode 100644 homeassistant/components/nfandroidtv/translations/fr.json create mode 100644 homeassistant/components/nfandroidtv/translations/it.json create mode 100644 homeassistant/components/nmap_tracker/translations/cs.json create mode 100644 homeassistant/components/prosegur/translations/ca.json create mode 100644 homeassistant/components/prosegur/translations/cs.json create mode 100644 homeassistant/components/prosegur/translations/de.json create mode 100644 homeassistant/components/prosegur/translations/et.json create mode 100644 homeassistant/components/prosegur/translations/fr.json create mode 100644 homeassistant/components/prosegur/translations/he.json create mode 100644 homeassistant/components/prosegur/translations/it.json create mode 100644 homeassistant/components/prosegur/translations/nl.json create mode 100644 homeassistant/components/prosegur/translations/no.json create mode 100644 homeassistant/components/prosegur/translations/pl.json create mode 100644 homeassistant/components/prosegur/translations/ru.json create mode 100644 homeassistant/components/prosegur/translations/zh-Hant.json create mode 100644 homeassistant/components/renault/translations/ca.json create mode 100644 homeassistant/components/renault/translations/cs.json create mode 100644 homeassistant/components/renault/translations/de.json create mode 100644 homeassistant/components/renault/translations/et.json create mode 100644 homeassistant/components/renault/translations/fr.json create mode 100644 homeassistant/components/renault/translations/he.json create mode 100644 homeassistant/components/renault/translations/it.json create mode 100644 homeassistant/components/renault/translations/nl.json create mode 100644 homeassistant/components/renault/translations/no.json create mode 100644 homeassistant/components/renault/translations/pl.json create mode 100644 homeassistant/components/renault/translations/ru.json create mode 100644 homeassistant/components/renault/translations/zh-Hant.json create mode 100644 homeassistant/components/sia/translations/fr.json create mode 100644 homeassistant/components/switcher_kis/translations/it.json create mode 100644 homeassistant/components/syncthing/translations/fr.json create mode 100644 homeassistant/components/wallbox/translations/fr.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/cs.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/fr.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/he.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/it.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/nl.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/no.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/pl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/fr.json create mode 100644 homeassistant/components/youless/translations/ca.json create mode 100644 homeassistant/components/youless/translations/cs.json create mode 100644 homeassistant/components/youless/translations/de.json create mode 100644 homeassistant/components/youless/translations/et.json create mode 100644 homeassistant/components/youless/translations/fr.json create mode 100644 homeassistant/components/youless/translations/he.json create mode 100644 homeassistant/components/youless/translations/it.json create mode 100644 homeassistant/components/youless/translations/nl.json create mode 100644 homeassistant/components/youless/translations/no.json create mode 100644 homeassistant/components/youless/translations/pl.json create mode 100644 homeassistant/components/youless/translations/ru.json create mode 100644 homeassistant/components/youless/translations/zh-Hant.json diff --git a/homeassistant/components/abode/translations/es-419.json b/homeassistant/components/abode/translations/es-419.json index 9de6d9d185a..6d380e5bb43 100644 --- a/homeassistant/components/abode/translations/es-419.json +++ b/homeassistant/components/abode/translations/es-419.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" }, "step": { @@ -15,7 +18,8 @@ }, "reauth_confirm": { "data": { - "password": "Contrase\u00f1a" + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" }, "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 92d5d5ef2c2..72d295da073 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave de API no v\u00e1lida", "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." }, "step": { diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json new file mode 100644 index 00000000000..ce5fa77543f --- /dev/null +++ b/homeassistant/components/adax/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index a8f036f6ed5..80164e30b54 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { "user": { "data": { + "account_id": "identifiant de compte", + "host": "H\u00f4te", "password": "Mot de passe" } } diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json new file mode 100644 index 00000000000..c0ccf6aff05 --- /dev/null +++ b/homeassistant/components/adax/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "account_id": "ID account", + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index 2a4a927b90a..0704e7d71ba 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -15,7 +15,7 @@ "name": "Name der Integration" }, "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "[void]" + "title": "AEMET OpenData" } } }, diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index bb1e792aa5e..4ad76320f03 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Recueillir les donn\u00e9es des stations m\u00e9t\u00e9orologiques AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json index b3018d53bc2..3050d6fb158 100644 --- a/homeassistant/components/airvisual/translations/sensor.fr.json +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -1,11 +1,20 @@ { "state": { "airvisual__pollutant_label": { - "co": "Monoxyde de carbone" + "co": "Monoxyde de carbone", + "n2": "Dioxyde d'azote", + "o3": "Ozone", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Dioxyde de soufre" }, "airvisual__pollutant_level": { "good": "Bon", - "hazardous": "Hasardeux" + "hazardous": "Hasardeux", + "moderate": "Mod\u00e9rer", + "unhealthy": "Malsain", + "unhealthy_sensitive": "Malsain pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s malsain" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json new file mode 100644 index 00000000000..28ac8c5c3e4 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "airvisual__pollutant_level": { + "good": "\u05d8\u05d5\u05d1", + "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.it.json b/homeassistant/components/airvisual/translations/sensor.it.json new file mode 100644 index 00000000000..7fb8b98215c --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.it.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Monossido di carbonio", + "n2": "Anidride nitrosa", + "o3": "Ozono", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Anidride solforosa" + }, + "airvisual__pollutant_level": { + "good": "Buono", + "hazardous": "Pericoloso", + "moderate": "Moderato", + "unhealthy": "Malsano", + "unhealthy_sensitive": "Malsano per gruppi sensibili", + "very_unhealthy": "Molto malsano" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.no.json b/homeassistant/components/airvisual/translations/sensor.no.json new file mode 100644 index 00000000000..86c95f8e8f2 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "airvisual__pollutant_label": { + "p1": "PM10", + "p2": "PM2.5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index c7e010e805e..6d8ee9c08c3 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -4,6 +4,7 @@ "arm_away": "Armer {entity_name} en mode \"sortie\"", "arm_home": "Armer {entity_name} en mode \"maison\"", "arm_night": "Armer {entity_name} en mode \"nuit\"", + "arm_vacation": "Armer {entity_name} vacances", "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" }, @@ -29,6 +30,7 @@ "armed_custom_bypass": "Arm\u00e9 avec exception personnalis\u00e9e", "armed_home": "Enclench\u00e9e (pr\u00e9sent)", "armed_night": "Enclench\u00e9 (nuit)", + "armed_vacation": "Arm\u00e9es vacances", "arming": "Activation", "disarmed": "D\u00e9sactiv\u00e9e", "disarming": "D\u00e9sactivation", diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json new file mode 100644 index 00000000000..bbb09edf763 --- /dev/null +++ b/homeassistant/components/ambee/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 API non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "cl\u00e9 API", + "description": "R\u00e9-authentifiez-vous avec votre compte Ambee." + } + }, + "user": { + "data": { + "api_key": "cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.fr.json b/homeassistant/components/ambee/translations/sensor.fr.json new file mode 100644 index 00000000000..76dc3fe6301 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.fr.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Haute", + "low": "Faible", + "moderate": "Mod\u00e9rer", + "very high": "Tr\u00e8s \u00e9lev\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.he.json b/homeassistant/components/ambee/translations/sensor.he.json new file mode 100644 index 00000000000..14ae06f2bc9 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.he.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "high": "\u05d2\u05d1\u05d5\u05d4", + "low": "\u05e0\u05de\u05d5\u05da", + "very high": "\u05d2\u05d1\u05d5\u05d4 \u05de\u05d0\u05d5\u05d3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/he.json b/homeassistant/components/arcam_fmj/translations/he.json index 0a4bd9ca12a..c07b9af0c67 100644 --- a/homeassistant/components/arcam_fmj/translations/he.json +++ b/homeassistant/components/arcam_fmj/translations/he.json @@ -5,10 +5,16 @@ "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "flow_title": "{host}", "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1- '{host}' \u05dc-Home Assistant?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1-`{host}` \u05dc-Home Assistant?" }, "user": { "data": { diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index fcd2157d321..6a860311a32 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -24,7 +24,7 @@ "username": "Benutzername" }, "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit deinem Router.", - "title": "" + "title": "AsusWRT" } } }, diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json index 5fb689b2562..aeb3c6a9f14 100644 --- a/homeassistant/components/august/translations/he.json +++ b/homeassistant/components/august/translations/he.json @@ -25,7 +25,7 @@ } }, "validation": { - "description": "\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} \u05e9\u05dc\u05da ({\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9}) \u05d5\u05d4\u05d6\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df" + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} ( {username} ) \u05d5\u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df" } } } diff --git a/homeassistant/components/auth/translations/he.json b/homeassistant/components/auth/translations/he.json index bc1826d4d79..6bbf472a14b 100644 --- a/homeassistant/components/auth/translations/he.json +++ b/homeassistant/components/auth/translations/he.json @@ -2,18 +2,18 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." + "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." }, "error": { "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { "init": { - "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify", + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05d7\u05d3 \u05de\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05d4\u05d4\u05d5\u05d3\u05e2\u05d5\u05ea:", "title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify" }, "setup": { - "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:", + "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea ** Notify. {notify_service} **. \u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4:", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4" } }, diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 31a75c3acdf..c345b1a94ce 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -35,19 +35,19 @@ "on": "\u05de\u05d7\u05d5\u05d1\u05e8" }, "door": { - "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "garage_door": { - "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", - "on": "\u05e4\u05ea\u05d5\u05d7\u05d4" + "off": "\u05e1\u05d2\u05d5\u05e8", + "on": "\u05e4\u05ea\u05d5\u05d7" }, "gas": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "heat": { - "off": "\u05e8\u05d2\u05d9\u05dc", + "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", "on": "\u05d7\u05dd" }, "light": { @@ -64,7 +64,7 @@ }, "motion": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "moving": { "off": "\u05dc\u05d0 \u05d6\u05d6", @@ -72,10 +72,10 @@ }, "occupancy": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "opening": { - "off": "\u05e0\u05e1\u05d2\u05e8", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { @@ -95,18 +95,18 @@ }, "smoke": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "sound": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "vibration": { "off": "\u05e0\u05e7\u05d9", - "on": "\u05d0\u05d5\u05ea\u05e8" + "on": "\u05d6\u05d5\u05d4\u05d4" }, "window": { - "off": "\u05e0\u05e1\u05d2\u05e8", + "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" } }, diff --git a/homeassistant/components/bosch_shc/translations/fr.json b/homeassistant/components/bosch_shc/translations/fr.json new file mode 100644 index 00000000000..38a48b269b4 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "pairing_failed": "L'appairage a \u00e9chou\u00e9\u00a0; veuillez v\u00e9rifier que le Bosch Smart Home Controller est en mode d'appairage (voyant clignotant) et que votre mot de passe est correct.", + "session_error": "Erreur de session\u00a0: l'API renvoie un r\u00e9sultat non-OK.", + "unknown": "Erreur inattendue" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Veuillez appuyer sur le bouton situ\u00e9 \u00e0 l'avant du Bosch Smart Home Controller jusqu'\u00e0 ce que le voyant commence \u00e0 clignoter.\n Pr\u00eat \u00e0 continuer \u00e0 configurer {model} @ {host} avec Home Assistant\u00a0?" + }, + "credentials": { + "data": { + "password": "Mot de passe du contr\u00f4leur Smart Home" + } + }, + "reauth_confirm": { + "description": "L'int\u00e9gration bosch_shc doit r\u00e9-authentifier votre compte", + "title": "R\u00e9authentification de l'int\u00e9gration" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurez votre Bosch Smart Home Controller pour permettre la surveillance et le contr\u00f4le avec Home Assistant.", + "title": "Param\u00e8tres d'authentification SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index b79ff0a7619..8126a04f21d 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", - "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" + "wrong_host": "Ung\u00fcltiger Hostname oder IP-Adresse" }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/buienradar/translations/fr.json b/homeassistant/components/buienradar/translations/fr.json index d9c2fadcbf7..19b7737ae11 100644 --- a/homeassistant/components/buienradar/translations/fr.json +++ b/homeassistant/components/buienradar/translations/fr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/calendar/translations/he.json b/homeassistant/components/calendar/translations/he.json index c80aaab5c1e..9a633670698 100644 --- a/homeassistant/components/calendar/translations/he.json +++ b/homeassistant/components/calendar/translations/he.json @@ -5,5 +5,5 @@ "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, - "title": "\u05dc\u05d5\u05bc\u05d7\u05b7 \u05e9\u05c1\u05b8\u05e0\u05b8\u05d4" + "title": "\u05dc\u05d5\u05d7 \u05e9\u05e0\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 3a9552980e6..d50e5b20684 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Google Cast \u05e0\u05d7\u05d5\u05e6\u05d4." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd." diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index 8e269db785b..123a1257d99 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -25,7 +25,7 @@ "data": { "timestep": "Minuten zwischen den Kurzvorhersagen" }, - "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", + "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", "title": "Aktualisiere ClimaCell-Optionen" } } diff --git a/homeassistant/components/cloudflare/translations/cs.json b/homeassistant/components/cloudflare/translations/cs.json index e20f26236be..8f88377860b 100644 --- a/homeassistant/components/cloudflare/translations/cs.json +++ b/homeassistant/components/cloudflare/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, @@ -11,6 +12,11 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API token" + } + }, "records": { "data": { "records": "Z\u00e1znamy" diff --git a/homeassistant/components/cloudflare/translations/fr.json b/homeassistant/components/cloudflare/translations/fr.json index be6d4c3e2b3..677dc8552fb 100644 --- a/homeassistant/components/cloudflare/translations/fr.json +++ b/homeassistant/components/cloudflare/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "unknown": "Erreur inattendue" }, @@ -11,6 +12,12 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "Jeton API", + "description": "R\u00e9-authentifiez-vous avec votre compte Cloudflare." + } + }, "records": { "data": { "records": "Enregistrements" diff --git a/homeassistant/components/co2signal/translations/cs.json b/homeassistant/components/co2signal/translations/cs.json new file mode 100644 index 00000000000..954168d1ee2 --- /dev/null +++ b/homeassistant/components/co2signal/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + }, + "country": { + "data": { + "country_code": "K\u00f3d zem\u011b" + } + }, + "user": { + "data": { + "api_key": "P\u0159\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/fr.json b/homeassistant/components/co2signal/translations/fr.json index 549124674dd..4b36bd3bd74 100644 --- a/homeassistant/components/co2signal/translations/fr.json +++ b/homeassistant/components/co2signal/translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "api_ratelimit": "Limite de d\u00e9bit de l\u2019API d\u00e9pass\u00e9e", "unknown": "Erreur inattendue" }, "error": { @@ -22,8 +24,10 @@ }, "user": { "data": { - "api_key": "Token d'acc\u00e8s" - } + "api_key": "Token d'acc\u00e8s", + "location": "Obtenir des donn\u00e9es pour" + }, + "description": "Visitez https://co2signal.com/ pour demander un jeton." } } } diff --git a/homeassistant/components/co2signal/translations/it.json b/homeassistant/components/co2signal/translations/it.json new file mode 100644 index 00000000000..0db63a1e912 --- /dev/null +++ b/homeassistant/components/co2signal/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "api_ratelimit": "Limite di frequenza API superato", + "unknown": "Errore imprevisto" + }, + "error": { + "api_ratelimit": "Limite di frequenza API superato", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine" + } + }, + "country": { + "data": { + "country_code": "Prefisso internazionale" + } + }, + "user": { + "data": { + "api_key": "Token di accesso", + "location": "Ottieni dati per" + }, + "description": "Visita https://co2signal.com/ per richiedere un token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json new file mode 100644 index 00000000000..24dc9ec4e14 --- /dev/null +++ b/homeassistant/components/coinbase/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 25d20fe8cf2..45360acd288 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -16,7 +16,7 @@ "currencies": "Kontostand W\u00e4hrungen", "exchange_rates": "Wechselkurse" }, - "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")", + "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt.", "title": "Coinbase API Schl\u00fcssel Details" } } diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json new file mode 100644 index 00000000000..e0ec1ae200d --- /dev/null +++ b/homeassistant/components/coinbase/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "cl\u00e9 API", + "api_token": "API secr\u00e8te", + "currencies": "Devises du solde du compte", + "exchange_rates": "Taux d'\u00e9change" + }, + "description": "Veuillez saisir les d\u00e9tails de votre cl\u00e9 API tels que fournis par Coinbase.", + "title": "D\u00e9tails de la cl\u00e9 de l'API Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", + "exchange_rate_unavaliable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Soldes du portefeuille \u00e0 d\u00e9clarer.", + "exchange_base": "Devise de base pour les capteurs de taux de change.", + "exchange_rate_currencies": "Taux de change \u00e0 d\u00e9clarer." + }, + "description": "Ajuster les options de Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index b13caef1976..e277eaf67db 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -16,7 +16,7 @@ "currencies": "Valuta's van rekeningsaldo", "exchange_rates": "Wisselkoersen" }, - "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase. Scheidt meerdere valuta's met een komma (bijv. \"BTC, EUR\")", + "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase.", "title": "Coinbase API Sleutel Details" } } diff --git a/homeassistant/components/configurator/translations/he.json b/homeassistant/components/configurator/translations/he.json index 7cc7aad41d7..aeff95ca5ce 100644 --- a/homeassistant/components/configurator/translations/he.json +++ b/homeassistant/components/configurator/translations/he.json @@ -1,9 +1,9 @@ { "state": { "_": { - "configure": "\u05d4\u05d2\u05d3\u05e8", - "configured": "\u05d4\u05d5\u05d2\u05d3\u05e8" + "configure": "\u05d4\u05d2\u05d3\u05e8\u05d4", + "configured": "\u05de\u05d5\u05d2\u05d3\u05e8" } }, - "title": "\u05e7\u05d5\u05e0\u05e4\u05d9\u05d2\u05d5\u05e8\u05d8\u05d5\u05e8" + "title": "\u05e7\u05d5\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/conversation/translations/he.json b/homeassistant/components/conversation/translations/he.json index eeccec319af..63cfb10abe8 100644 --- a/homeassistant/components/conversation/translations/he.json +++ b/homeassistant/components/conversation/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05e9\u05c2\u05b4\u05d9\u05d7\u05b8\u05d4" + "title": "\u05e9\u05d9\u05d7\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/fr.json b/homeassistant/components/coronavirus/translations/fr.json index 21a72d80f61..9a9a960cf31 100644 --- a/homeassistant/components/coronavirus/translations/fr.json +++ b/homeassistant/components/coronavirus/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ce pays est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Ce pays est d\u00e9j\u00e0 configur\u00e9.", + "cannot_connect": "\u00c9chec de connexion" }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index fce73cc1698..5dad593467c 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -6,11 +6,11 @@ }, "state": { "_": { - "closed": "\u05e0\u05e1\u05d2\u05e8", + "closed": "\u05e1\u05d2\u05d5\u05e8", "closing": "\u05e1\u05d5\u05d2\u05e8", "open": "\u05e4\u05ea\u05d5\u05d7", "opening": "\u05e4\u05d5\u05ea\u05d7", - "stopped": "\u05e2\u05e6\u05d5\u05e8" + "stopped": "\u05e2\u05e6\u05e8" } }, "title": "\u05d5\u05d9\u05dc\u05d5\u05df" diff --git a/homeassistant/components/demo/translations/select.fr.json b/homeassistant/components/demo/translations/select.fr.json new file mode 100644 index 00000000000..d2b214e4078 --- /dev/null +++ b/homeassistant/components/demo/translations/select.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Vitesse de la lumi\u00e8re", + "ludicrous_speed": "Vitesse ridicule", + "ridiculous_speed": "Vitesse ridicule" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/he.json b/homeassistant/components/device_tracker/translations/he.json index 5db22ed4071..2f3ccc1ec1e 100644 --- a/homeassistant/components/device_tracker/translations/he.json +++ b/homeassistant/components/device_tracker/translations/he.json @@ -5,5 +5,5 @@ "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea" } }, - "title": "\u05de\u05e2\u05e7\u05d1 \u05de\u05db\u05e9\u05d9\u05e8" + "title": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json index 44906c51207..54169346968 100644 --- a/homeassistant/components/devolo_home_control/translations/cs.json +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index d0f806c042e..bc9a5715238 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e." + "already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "reauth_failed": "Veuillez utiliser le m\u00eame utilisateur mydevolo que pr\u00e9c\u00e9demment." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index f9ab82881a1..2ac09df14fd 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9/ \u05de\u05d6\u05d4\u05d4 devolo" + "username": "\u05d3\u05d5\u05d0\"\u05dc / \u05de\u05d6\u05d4\u05d4 devolo" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/directv/translations/he.json b/homeassistant/components/directv/translations/he.json index f057c4e4629..bc28ff4eba5 100644 --- a/homeassistant/components/directv/translations/he.json +++ b/homeassistant/components/directv/translations/he.json @@ -9,6 +9,14 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index 3f462c74c05..0f348acdbff 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -1,23 +1,46 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_communicate": "\u00c9chec de la communication", + "cannot_connect": "\u00c9chec de connexion" }, "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_communicate": "\u00c9chec de la communication", + "cannot_connect": "\u00c9chec de connexion", "one": "Vide", "other": "Vide" }, "step": { "one": "", "other": "Autre", + "setup_network": { + "data": { + "dsmr_version": "S\u00e9lectionner la version DSMR", + "host": "H\u00f4te", + "port": "Port" + }, + "title": "S\u00e9lectionner l'adresse de connexion" + }, "setup_serial": { "data": { + "dsmr_version": "S\u00e9lectionner la version DSMR", "port": "S\u00e9lectionner un appareil" }, "title": "Appareil" }, "setup_serial_manual_path": { + "data": { + "port": "Chemin du p\u00e9riph\u00e9rique USB" + }, "title": "Chemin" + }, + "user": { + "data": { + "type": "Type de connexion" + }, + "title": "S\u00e9lectionner le type de connexion" } } }, diff --git a/homeassistant/components/emonitor/translations/he.json b/homeassistant/components/emonitor/translations/he.json index 4ec15aa12cb..77bd85b18b8 100644 --- a/homeassistant/components/emonitor/translations/he.json +++ b/homeassistant/components/emonitor/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/energy/translations/cs.json b/homeassistant/components/energy/translations/cs.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/de.json b/homeassistant/components/energy/translations/de.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/fr.json b/homeassistant/components/energy/translations/fr.json new file mode 100644 index 00000000000..f947a07baec --- /dev/null +++ b/homeassistant/components/energy/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "\u00c9nergie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/he.json b/homeassistant/components/energy/translations/he.json new file mode 100644 index 00000000000..3c61aad6089 --- /dev/null +++ b/homeassistant/components/energy/translations/he.json @@ -0,0 +1,3 @@ +{ + "title": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/it.json b/homeassistant/components/energy/translations/it.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json new file mode 100644 index 00000000000..53457a69447 --- /dev/null +++ b/homeassistant/components/energy/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Energie" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pl.json b/homeassistant/components/energy/translations/pl.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ru.json b/homeassistant/components/energy/translations/ru.json new file mode 100644 index 00000000000..b351e407168 --- /dev/null +++ b/homeassistant/components/energy/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u042d\u043d\u0435\u0440\u0433\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hant.json b/homeassistant/components/energy/translations/zh-Hant.json new file mode 100644 index 00000000000..bae50fae66e --- /dev/null +++ b/homeassistant/components/energy/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u80fd\u6e90" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json index be1d5f3bca3..9587739e88a 100644 --- a/homeassistant/components/enphase_envoy/translations/fr.json +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/flipr/translations/cs.json b/homeassistant/components/flipr/translations/cs.json new file mode 100644 index 00000000000..29c2ebc1713 --- /dev/null +++ b/homeassistant/components/flipr/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json index 32769aab9b7..ec9260aa8a7 100644 --- a/homeassistant/components/flipr/translations/fr.json +++ b/homeassistant/components/flipr/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "no_flipr_id_found": "Aucun identifiant Flipr n'est associ\u00e9 \u00e0 votre compte pour le moment. Vous devez d'abord v\u00e9rifier qu'il fonctionne avec l'application mobile de Flipr.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/flipr/translations/he.json b/homeassistant/components/flipr/translations/he.json index 85872f14f2c..ecb8a74bc6f 100644 --- a/homeassistant/components/flipr/translations/he.json +++ b/homeassistant/components/flipr/translations/he.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/flipr/translations/it.json b/homeassistant/components/flipr/translations/it.json new file mode 100644 index 00000000000..399d4c6b9d3 --- /dev/null +++ b/homeassistant/components/flipr/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "no_flipr_id_found": "Nessun ID flipr associato al tuo account per ora. Dovresti prima verificare che funzioni con l'app mobile di Flipr.", + "unknown": "Errore imprevisto" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "ID Flipr" + }, + "description": "Scegli il tuo ID Flipr nell'elenco", + "title": "Scegli il tuo Flipr" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "description": "Connettiti usando il tuo account Flipr.", + "title": "Connettiti a Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pl.json b/homeassistant/components/flipr/translations/pl.json index 1095c33a83e..436061f7f61 100644 --- a/homeassistant/components/flipr/translations/pl.json +++ b/homeassistant/components/flipr/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "no_flipr_id_found": "Brak identyfikatora flipr powi\u0105zanego z Twoim kontem. Sprawd\u017a najpierw, czy dzia\u0142a z aplikacj\u0105 mobiln\u0105 Flipr.", + "no_flipr_id_found": "Brak identyfikatora Flipr powi\u0105zanego z Twoim kontem. Sprawd\u017a najpierw, czy dzia\u0142a z aplikacj\u0105 mobiln\u0105 Flipr.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -22,7 +22,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, - "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta Flipr.", + "description": "Po\u0142\u0105cz, u\u017cywaj\u0105c swojego konta Flipr.", "title": "Po\u0142\u0105czenie z Flipr" } } diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index a111d66b937..5fe7fcf2ca4 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", diff --git a/homeassistant/components/forecast_solar/translations/fr.json b/homeassistant/components/forecast_solar/translations/fr.json index a6091b9c21b..efd9f7be3a6 100644 --- a/homeassistant/components/forecast_solar/translations/fr.json +++ b/homeassistant/components/forecast_solar/translations/fr.json @@ -3,8 +3,28 @@ "step": { "user": { "data": { + "azimuth": "Azimut (360 degr\u00e9s, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ouest)", + "declination": "D\u00e9clinaison (0 = horizontale, 90 = verticale)", + "latitude": "Latitude", + "longitude": "Longitude", + "modules power": "Puissance de cr\u00eate totale en watts de vos modules solaires", "name": "Nom" - } + }, + "description": "Remplissez les donn\u00e9es de vos panneaux solaires. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation si un champ n'est pas clair." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Cl\u00e9 API Forecast.Solar (facultatif)", + "azimuth": "Azimut (360 degr\u00e9s, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ouest)", + "damping": "Facteur d'amortissement : ajuste les r\u00e9sultats matin et soir", + "declination": "D\u00e9clinaison (0 = horizontale, 90 = verticale)", + "modules power": "Puissance de cr\u00eate totale en watts de vos modules solaires" + }, + "description": "Ces valeurs permettent de peaufiner le r\u00e9sultat Solar.Forecast. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation si un champ n'est pas clair." } } } diff --git a/homeassistant/components/freedompro/translations/fr.json b/homeassistant/components/freedompro/translations/fr.json index 4822faaef86..6667226a206 100644 --- a/homeassistant/components/freedompro/translations/fr.json +++ b/homeassistant/components/freedompro/translations/fr.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, "step": { "user": { + "data": { + "api_key": "cl\u00e9 API" + }, + "description": "Veuillez saisir la cl\u00e9 API obtenue sur https://home.freedompro.eu", "title": "Cl\u00e9 API Freedompro" } } diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index e0fa5dd3e8c..6518b5ed20c 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9 ", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", "connection_error": "Erreur de connexion", "invalid_auth": "Authentification invalide" }, @@ -35,6 +39,25 @@ }, "description": "Configuration de FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\nMinimum requis: nom d'utilisateur, mot de passe.", "title": "Configuration FRITZ!Box Tools - obligatoire" + }, + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Configurer FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\n Minimum requis : nom d'utilisateur, mot de passe.", + "title": "Configurer les outils de FRITZ!Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'" + } } } } diff --git a/homeassistant/components/garages_amsterdam/translations/fr.json b/homeassistant/components/garages_amsterdam/translations/fr.json new file mode 100644 index 00000000000..68530899d1e --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "garage_name": "Nom du garage" + }, + "title": "Choisisser un garage \u00e0 surveiller" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 7bd4929ad92..bb6a777b6be 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +11,10 @@ "unknown": "Erreur inconnue" }, "step": { + "confirm_discovery": { + "description": "La r\u00e9servation DHCP sur votre routeur est recommand\u00e9e. S'il n'est pas configur\u00e9, l'appareil peut devenir indisponible jusqu'\u00e0 ce que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", + "title": "Objectif Z\u00e9ro Y\u00e9ti" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json index 79f216738c4..94cee628a79 100644 --- a/homeassistant/components/gogogate2/translations/fr.json +++ b/homeassistant/components/gogogate2/translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index f0aa3b0c2d8..0ca969e6812 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -1,13 +1,13 @@ { "state": { "_": { - "closed": "\u05e0\u05e1\u05d2\u05e8", + "closed": "\u05e1\u05d2\u05d5\u05e8", "home": "\u05d1\u05d1\u05d9\u05ea", "locked": "\u05e0\u05e2\u05d5\u05dc", "not_home": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", "off": "\u05db\u05d1\u05d5\u05d9", "ok": "\u05ea\u05e7\u05d9\u05df", - "on": "\u05d3\u05dc\u05d5\u05e7", + "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "open": "\u05e4\u05ea\u05d5\u05d7", "problem": "\u05d1\u05e2\u05d9\u05d4", "unlocked": "\u05e4\u05ea\u05d5\u05d7" diff --git a/homeassistant/components/growatt_server/translations/cs.json b/homeassistant/components/growatt_server/translations/cs.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/fr.json b/homeassistant/components/growatt_server/translations/fr.json new file mode 100644 index 00000000000..1ad47166f8d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_plants": "Aucune plante n'a \u00e9t\u00e9 trouv\u00e9e sur ce compte" + }, + "error": { + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plante" + }, + "title": "S\u00e9lectionner votre plante" + }, + "user": { + "data": { + "name": "Nom", + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + }, + "title": "Entrer vos informations Growatt" + } + } + }, + "title": "Serveur Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/he.json b/homeassistant/components/growatt_server/translations/he.json index 14b47532075..8d430b5f4b2 100644 --- a/homeassistant/components/growatt_server/translations/he.json +++ b/homeassistant/components/growatt_server/translations/he.json @@ -4,6 +4,11 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "plant": { + "data": { + "plant_id": "\u05e6\u05de\u05d7" + } + }, "user": { "data": { "name": "\u05e9\u05dd", diff --git a/homeassistant/components/growatt_server/translations/it.json b/homeassistant/components/growatt_server/translations/it.json index 19862f82d83..a3160c4164b 100644 --- a/homeassistant/components/growatt_server/translations/it.json +++ b/homeassistant/components/growatt_server/translations/it.json @@ -17,6 +17,7 @@ "data": { "name": "Nome", "password": "Password", + "url": "URL", "username": "Utente" }, "title": "Inserisci le tue informazioni Growatt" diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index ca5635a17b7..62ffae35776 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -6,6 +6,9 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer cet appareil Guardian\u00a0?" + }, "user": { "data": { "ip_address": "Adresse IP", diff --git a/homeassistant/components/harmony/translations/he.json b/homeassistant/components/harmony/translations/he.json index 1331c17e961..49470b50ca9 100644 --- a/homeassistant/components/harmony/translations/he.json +++ b/homeassistant/components/harmony/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json index 8e7d9e9bc24..8cb47ae3fec 100644 --- a/homeassistant/components/home_plus_control/translations/de.json +++ b/homeassistant/components/home_plus_control/translations/de.json @@ -17,5 +17,5 @@ } } }, - "title": "" + "title": "Legrand Home+ Steuerung" } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 0fb983f1a20..c9afa13fb85 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domini da includere" }, - "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale TV e telecamera.", + "description": "Scegli i domini da includere. Tutte le entit\u00e0 supportate nel dominio saranno incluse. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e telecamera.", "title": "Seleziona i domini da includere" } } diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 5ae7a0faafd..faf970f88eb 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Code HomeKit incorrect. S'il vous pla\u00eet v\u00e9rifier et essayez \u00e0 nouveau.", + "insecure_setup_code": "Le code de configuration demand\u00e9 n'est pas s\u00e9curis\u00e9 en raison de sa nature triviale. Cet accessoire ne r\u00e9pond pas aux exigences de s\u00e9curit\u00e9 de base.", "max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.", "pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.", "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Autoriser le jumelage avec des codes de configuration non s\u00e9curis\u00e9s.", "pairing_code": "Code d\u2019appairage" }, "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", diff --git a/homeassistant/components/honeywell/translations/cs.json b/homeassistant/components/honeywell/translations/cs.json new file mode 100644 index 00000000000..25ad431df4e --- /dev/null +++ b/homeassistant/components/honeywell/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json index 506e14ab26f..b9b625eb589 100644 --- a/homeassistant/components/honeywell/translations/fr.json +++ b/homeassistant/components/honeywell/translations/fr.json @@ -1,10 +1,16 @@ { "config": { + "error": { + "invalid_auth": "Authentification incorrecte" + }, "step": { "user": { "data": { - "password": "Mot de passe" - } + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez saisir les informations d'identification utilis\u00e9es pour vous connecter \u00e0 mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (\u00c9tats-Unis)" } } } diff --git a/homeassistant/components/honeywell/translations/it.json b/homeassistant/components/honeywell/translations/it.json new file mode 100644 index 00000000000..52c828ddcde --- /dev/null +++ b/homeassistant/components/honeywell/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le credenziali utilizzate per accedere a mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index df7e6c2e380..da8fbcbd115 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -35,7 +35,8 @@ "name": "Nom du service de notification (red\u00e9marrage requis)", "recipient": "Destinataires des notifications SMS", "track_new_devices": "Suivre les nouveaux appareils", - "track_wired_clients": "Suivre les clients du r\u00e9seau filaire" + "track_wired_clients": "Suivre les clients du r\u00e9seau filaire", + "unauthenticated_mode": "Mode non authentifi\u00e9 (le changement n\u00e9cessite un rechargement)" } } } diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index f19c5ec7a34..e9dd546840f 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -11,13 +11,13 @@ "unknown": "Une erreur inconnue s'est produite" }, "error": { - "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant", + "linking": "Erreur inattendue", "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." }, "step": { "init": { "data": { - "host": "Nom d'h\u00f4te ou adresse IP" + "host": "H\u00f4te" }, "title": "Choisissez le pont Philips Hue" }, diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json index a1bd06078c6..68ea30b293f 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/fr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "unknown": "Erreur inattendue" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Voulez-vous configurer {name} ({host})?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json index c6610f79e77..8bd6c87154c 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/he.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/input_boolean/translations/he.json b/homeassistant/components/input_boolean/translations/he.json index 08bdc30a602..b5d50c10627 100644 --- a/homeassistant/components/input_boolean/translations/he.json +++ b/homeassistant/components/input_boolean/translations/he.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05e7\u05dc\u05d8 \u05d1\u05d5\u05dc\u05d9\u05d0\u05e0\u05d9" diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 01f2145cd12..0bd04cd14b1 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -36,5 +36,13 @@ "title": "Options ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY connect\u00e9", + "host_reachable": "H\u00f4te joignable", + "last_heartbeat": "Heure du dernier pulsation", + "websocket_status": "\u00c9tat du socket d'\u00e9v\u00e9nement" + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/fr.json b/homeassistant/components/keenetic_ndms2/translations/fr.json index bf3ebbf5b22..74685da509b 100644 --- a/homeassistant/components/keenetic_ndms2/translations/fr.json +++ b/homeassistant/components/keenetic_ndms2/translations/fr.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_udn": "Les informations de d\u00e9couverte SSDP n'ont pas d'UDN", + "not_keenetic_ndms2": "L'\u00e9l\u00e9ment d\u00e9couvert n'est pas un routeur Keenetic" }, "error": { "cannot_connect": "\u00c9chec de connexion" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json index 04e02ee88a0..5b992705068 100644 --- a/homeassistant/components/kodi/translations/he.json +++ b/homeassistant/components/kodi/translations/he.json @@ -22,7 +22,7 @@ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05d1-Kodi. \u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05dd \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd." }, "discovery_confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9 ((`{name}`) \u05dc-Home Assistant?", + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9 (`{name}`) \u05dc-Home Assistant?", "title": "\u05d2\u05d9\u05dc\u05d4 \u05d0\u05ea \u05e7\u05d5\u05d3\u05d9" }, "user": { diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index 9e791916df7..0a436bc2d3c 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -22,14 +22,30 @@ } }, "options": { + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "step": { "options_binary": { "data": { "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" } }, + "options_digital": { + "data": { + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } + }, "options_io": { "description": "\u05d4\u05ea\u05d2\u05dc\u05d4 {model} \u05d1-{host} . \u05d1\u05d7\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d1\u05e1\u05d9\u05e1 \u05e9\u05dc \u05db\u05dc \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05dc\u05de\u05d8\u05d4 - \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d6\u05d4 \u05e2\u05e9\u05d5\u05d9 \u05dc\u05d0\u05e4\u05e9\u05e8 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd (\u05de\u05d2\u05e2\u05d9\u05dd \u05e4\u05ea\u05d5\u05d7\u05d9\u05dd/\u05e1\u05d2\u05d5\u05e8\u05d9\u05dd), \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d3\u05d9\u05d2\u05d9\u05d8\u05dc\u05d9\u05d9\u05dd (dht \u05d5-ds18b20), \u05d0\u05d5 \u05d9\u05e6\u05d9\u05d0\u05d5\u05ea \u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d7\u05dc\u05e4\u05d4. \u05ea\u05d5\u05db\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05e4\u05d5\u05e8\u05d8\u05d5\u05ea \u05d1\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05d1\u05d0\u05d9\u05dd." + }, + "options_switch": { + "data": { + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } } } } diff --git a/homeassistant/components/kraken/translations/fr.json b/homeassistant/components/kraken/translations/fr.json new file mode 100644 index 00000000000..1aa7fdfbf54 --- /dev/null +++ b/homeassistant/components/kraken/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "one": "UN", + "other": "AUTRE" + }, + "step": { + "user": { + "data": { + "one": "UN", + "other": "AUTRE" + }, + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour", + "tracked_asset_pairs": "Paires d'actifs suivis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/he.json b/homeassistant/components/kraken/translations/he.json index 4676729e600..2be0837c966 100644 --- a/homeassistant/components/kraken/translations/he.json +++ b/homeassistant/components/kraken/translations/he.json @@ -3,10 +3,31 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "step": { "user": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e2\u05d3\u05db\u05d5\u05df" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json index 89459d1829f..b8ca0adb3d4 100644 --- a/homeassistant/components/litejet/translations/fr.json +++ b/homeassistant/components/litejet/translations/fr.json @@ -15,5 +15,15 @@ "title": "Connectez-vous \u00e0 LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transition par d\u00e9faut (secondes)" + }, + "title": "Configurer LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/it.json b/homeassistant/components/litejet/translations/it.json index 5b3dc46753d..0b34e6fb365 100644 --- a/homeassistant/components/litejet/translations/it.json +++ b/homeassistant/components/litejet/translations/it.json @@ -15,5 +15,15 @@ "title": "Connetti a LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transizione predefinita (secondi)" + }, + "title": "Configura LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index db23120b40d..9eb02fc3811 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "create_entry": { "default": "Authentification r\u00e9ussie" @@ -12,7 +13,8 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { - "description": "L'int\u00e9gration Lyric doit authentifier \u00e0 nouveau votre compte." + "description": "L'int\u00e9gration Lyric doit authentifier \u00e0 nouveau votre compte.", + "title": "R\u00e9authentification de l'int\u00e9gration" } } } diff --git a/homeassistant/components/meteoclimatic/translations/fr.json b/homeassistant/components/meteoclimatic/translations/fr.json new file mode 100644 index 00000000000..ae087db2e6e --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "unknown": "Erreur inattendue" + }, + "error": { + "not_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "step": { + "user": { + "data": { + "code": "Code de la station" + }, + "description": "Entrer le code de la station m\u00e9t\u00e9orologique (par exemple, ESCAT4300000043206)", + "title": "M\u00e9t\u00e9oclimatique" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 1c11717c1b2..1a9c3b5d352 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -16,9 +16,9 @@ "password": "Passwort", "port": "Port", "username": "Benutzername", - "verify_ssl": "Verwenden Sie SSL" + "verify_ssl": "SSL verwenden" }, - "title": "Richten Sie den Mikrotik Router ein" + "title": "Mikrotik Router einrichten" } } }, @@ -28,7 +28,7 @@ "data": { "arp_ping": "ARP-Ping aktivieren", "detection_time": "Heimintervall ber\u00fccksichtigen", - "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" + "force_dhcp": "Scannen mit DHCP erzwingen" } } } diff --git a/homeassistant/components/modern_forms/translations/fr.json b/homeassistant/components/modern_forms/translations/fr.json new file mode 100644 index 00000000000..d68f4a7f680 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurer votre ventilateur Modern Forms pour l'int\u00e9grer \u00e0 Home Assistant." + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le fan de Modern Forms nomm\u00e9 ` {name} ` \u00e0 Home Assistant\u00a0?", + "title": "D\u00e9couverte du dispositif de ventilateur Modern Forms" + } + } + }, + "title": "Formes modernes" +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index 338bc392bd5..b8d79b683a6 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -1,18 +1,27 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { - "invalid_url": "URL invalide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "invalid_url": "URL invalide", + "unknown": "Erreur inattendue" }, "step": { + "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour vous connecter au service motionEye fourni par le module compl\u00e9mentaire\u00a0: {addon}\u00a0?", + "title": "motionEye via le module compl\u00e9mentaire Home Assistant" + }, "user": { "data": { "admin_password": "Admin Mot de passe", "admin_username": "Admin Nom d'utilisateur", "surveillance_password": "Surveillance Mot de passe", - "surveillance_username": "Surveillance Nom d'utilisateur" + "surveillance_username": "Surveillance Nom d'utilisateur", + "url": "URL" } } } diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index e9a6bc60b82..c07e3710645 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "MyQ est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "MyQ est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index bb6a1ed7bfe..cfef6f7d363 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -76,5 +76,5 @@ } } }, - "title": "" + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json index 0c58af2a800..1800e6da508 100644 --- a/homeassistant/components/nam/translations/fr.json +++ b/homeassistant/components/nam/translations/fr.json @@ -4,6 +4,10 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "device_unsupported": "L'appareil n'est pas pris en charge." }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, "flow_title": "{nom}", "step": { "confirm_discovery": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index dab438c24a8..a3b5411b536 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", diff --git a/homeassistant/components/nexia/translations/fr.json b/homeassistant/components/nexia/translations/fr.json index 8082f912bed..5cec9b66836 100644 --- a/homeassistant/components/nexia/translations/fr.json +++ b/homeassistant/components/nexia/translations/fr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marque", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/nfandroidtv/translations/cs.json b/homeassistant/components/nfandroidtv/translations/cs.json new file mode 100644 index 00000000000..b268d8945a0 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/fr.json b/homeassistant/components/nfandroidtv/translations/fr.json new file mode 100644 index 00000000000..6d00852889b --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom" + }, + "description": "Cette int\u00e9gration n\u00e9cessite l'application Notifications pour Android TV. \n\nPour Android TV\u00a0: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPour Fire TV\u00a0: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nVous devez configurer soit une r\u00e9servation DHCP sur votre routeur (reportez-vous au manuel d'utilisation de votre routeur) soit une adresse IP statique sur l'appareil. Sinon, l'appareil finira par devenir indisponible.", + "title": "Notifications pour Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/it.json b/homeassistant/components/nfandroidtv/translations/it.json new file mode 100644 index 00000000000..3b8d089b5a5 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome" + }, + "description": "Questa integrazione richiede l'app Notifiche per Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n\u00c8 necessario impostare la prenotazione DHCP sul router (fare riferimento al manuale utente del router) o un indirizzo IP statico sul dispositivo. In caso contrario, il dispositivo alla fine non sar\u00e0 pi\u00f9 disponibile.", + "title": "Notifiche per Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/nl.json b/homeassistant/components/nfandroidtv/translations/nl.json index b231bd00c3c..acd936abe70 100644 --- a/homeassistant/components/nfandroidtv/translations/nl.json +++ b/homeassistant/components/nfandroidtv/translations/nl.json @@ -13,6 +13,7 @@ "host": "Host", "name": "Naam" }, + "description": "Voor deze integratie is de app Notifications for Android TV vereist.\n\nVoor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nVoor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nU moet een DHCP-reservering op uw router instellen (raadpleeg de gebruikershandleiding van uw router) of een statisch IP-adres op het apparaat instellen. Zo niet, dan zal het apparaat uiteindelijk onbeschikbaar worden.", "title": "Meldingen voor Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/pl.json b/homeassistant/components/nfandroidtv/translations/pl.json index 597d8bb9200..4dd742b7c1f 100644 --- a/homeassistant/components/nfandroidtv/translations/pl.json +++ b/homeassistant/components/nfandroidtv/translations/pl.json @@ -13,7 +13,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", + "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub ustawi\u0107 statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", "title": "Powiadomienia dla Android TV / Fire TV" } } diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json new file mode 100644 index 00000000000..1a0d0ae0b53 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json index f4302a4173c..69d7d58f2e6 100644 --- a/homeassistant/components/nmap_tracker/translations/fr.json +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -1,12 +1,40 @@ { + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_hosts": "H\u00f4tes invalides" + }, + "step": { + "user": { + "data": { + "exclude": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 exclure de l'analyse", + "home_interval": "Nombre minimum de minutes entre les analyses des appareils actifs (pr\u00e9server la batterie)", + "hosts": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 analyser", + "scan_options": "Options d'analyse brutes configurables pour Nmap" + }, + "description": "Configurer les h\u00f4tes \u00e0 analyser par Nmap. L'adresse r\u00e9seau et les exclusions peuvent \u00eatre des adresses IP (192.168.1.1), des r\u00e9seaux IP (192.168.0.0/24) ou des plages IP (192.168.1.0-32)." + } + } + }, "options": { + "error": { + "invalid_hosts": "H\u00f4tes invalides" + }, "step": { "init": { "data": { + "exclude": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 exclure de l'analyse", + "home_interval": "Nombre minimum de minutes entre les analyses des appareils actifs (pr\u00e9server la batterie)", + "hosts": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 analyser", "interval_seconds": "Intervalle d\u2019analyse", + "scan_options": "Options d'analyse brutes configurables pour Nmap", "track_new_devices": "Suivre les nouveaux appareils" - } + }, + "description": "Configurer les h\u00f4tes \u00e0 analyser par Nmap. L'adresse r\u00e9seau et les exclusions peuvent \u00eatre des adresses IP (192.168.1.1),R\u00e9seaux IP (192.168.0.0/24) ou plages IP (192.168.1.0-32)." } } - } + }, + "title": "Traqueur Nmap" } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index d8d1cf89611..76eb733db3d 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -18,6 +18,16 @@ }, "title": "Configurer l'authentification" }, + "configure": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configurer le p\u00e9riph\u00e9rique ONVIF" + }, "configure_profile": { "data": { "include": "Cr\u00e9er une entit\u00e9 cam\u00e9ra" @@ -40,6 +50,9 @@ "title": "Configurer l\u2019appareil ONVIF" }, "user": { + "data": { + "auto": "Rechercher automatiquement" + }, "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez-vous qu\u2019ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", "title": "Configuration de l'appareil ONVIF" } diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 290effbc48e..6d1229f1a5d 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05dc\u05da.", + "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d9\u05e9 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da.", "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF." }, "error": { diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json index 7864218bc3b..270d8744b96 100644 --- a/homeassistant/components/ovo_energy/translations/he.json +++ b/homeassistant/components/ovo_energy/translations/he.json @@ -17,7 +17,8 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da." + "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05d7\u05e9\u05d1\u05d5\u05df \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc OVO" } } } diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index eb16bb92271..8cc5d187743 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 l'appareil de s'allumer" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Autoriser l'utilisation du service de notification de donn\u00e9es." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 044b0a72771..75e35a951de 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index a89120b85ab..db3eeef2d53 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -10,6 +10,9 @@ }, "flow_title": "{name}", "step": { + "user": { + "description": "\u05de\u05d5\u05e6\u05e8:" + }, "user_gateway": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 0f82f949ede..5ce9313b442 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -13,7 +13,7 @@ "password": "Passwort" }, "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", - "title": "" + "title": "PoolSense" } } } diff --git a/homeassistant/components/prosegur/translations/ca.json b/homeassistant/components/prosegur/translations/ca.json new file mode 100644 index 00000000000..a6c7c925a25 --- /dev/null +++ b/homeassistant/components/prosegur/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Torna a autenticar-te amb el compte de Prosegur.", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, + "user": { + "data": { + "country": "Pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/cs.json b/homeassistant/components/prosegur/translations/cs.json new file mode 100644 index 00000000000..13c0827ff40 --- /dev/null +++ b/homeassistant/components/prosegur/translations/cs.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/de.json b/homeassistant/components/prosegur/translations/de.json new file mode 100644 index 00000000000..aa5667d8d54 --- /dev/null +++ b/homeassistant/components/prosegur/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Authentifiziere dich erneut mit deinem Prosegur-Konto.", + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "country": "Land", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/et.json b/homeassistant/components/prosegur/translations/et.json new file mode 100644 index 00000000000..bc5a1a5a7ea --- /dev/null +++ b/homeassistant/components/prosegur/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu viga" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Taastuvasta oma Prosegur kontoga.", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, + "user": { + "data": { + "country": "Riik", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/fr.json b/homeassistant/components/prosegur/translations/fr.json new file mode 100644 index 00000000000..7c0d361da6a --- /dev/null +++ b/homeassistant/components/prosegur/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "R\u00e9-authentifiez-vous avec le compte Prosegur.", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "user": { + "data": { + "country": "Pays", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/he.json b/homeassistant/components/prosegur/translations/he.json new file mode 100644 index 00000000000..89a7445f2c3 --- /dev/null +++ b/homeassistant/components/prosegur/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "country": "\u05de\u05d3\u05d9\u05e0\u05d4", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/it.json b/homeassistant/components/prosegur/translations/it.json new file mode 100644 index 00000000000..79409045d7b --- /dev/null +++ b/homeassistant/components/prosegur/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Eseguire nuovamente l'autenticazione con l'account Prosegur.", + "password": "Password", + "username": "Nome utente" + } + }, + "user": { + "data": { + "country": "Nazione", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/nl.json b/homeassistant/components/prosegur/translations/nl.json new file mode 100644 index 00000000000..d87556b0742 --- /dev/null +++ b/homeassistant/components/prosegur/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Verifieer opnieuw met Prosegur-account.", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, + "user": { + "data": { + "country": "Land", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/no.json b/homeassistant/components/prosegur/translations/no.json new file mode 100644 index 00000000000..5732bb920b2 --- /dev/null +++ b/homeassistant/components/prosegur/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/pl.json b/homeassistant/components/prosegur/translations/pl.json new file mode 100644 index 00000000000..342c17222b2 --- /dev/null +++ b/homeassistant/components/prosegur/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Prosegur.", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "country": "Kraj", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/ru.json b/homeassistant/components/prosegur/translations/ru.json new file mode 100644 index 00000000000..c75f3e572c6 --- /dev/null +++ b/homeassistant/components/prosegur/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Prosegur.", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "user": { + "data": { + "country": "\u0421\u0442\u0440\u0430\u043d\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/zh-Hant.json b/homeassistant/components/prosegur/translations/zh-Hant.json new file mode 100644 index 00000000000..501e979448e --- /dev/null +++ b/homeassistant/components/prosegur/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u91cd\u65b0\u8a8d\u8b49 Prosegur \u5e33\u865f\u3002", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "user": { + "data": { + "country": "\u570b\u5bb6", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index 837dd13b925..e9543da8206 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea." + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" @@ -14,7 +14,7 @@ "link": { "data": { "code": "\u05e7\u05d5\u05d3 PIN", - "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP", + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "name": "\u05e9\u05dd", "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" }, diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 626382fcb6f..545a3d2cd9f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -11,7 +11,7 @@ "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ", + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "title": "Sensoreinrichtung" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 5386529e43a..f8511a80579 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -7,11 +7,26 @@ "user": { "data": { "name": "Nom du capteur", + "power": "Puissance souscrite (kW)", + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", "tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)" }, "description": "Ce capteur utilise l'API officielle pour obtenir la [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espagne. \n Pour une explication plus pr\u00e9cise, visitez la [documentation d'int\u00e9gration] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n S\u00e9lectionnez le tarif contract\u00e9 en fonction du nombre de p\u00e9riodes de facturation par jour: \n - 1 p\u00e9riode: normale \n - 2 p\u00e9riodes: discrimination (tarif \u00e0 la nuit) \n - 3 p\u00e9riodes: voiture \u00e9lectrique (tarif \u00e0 la nuit sur 3 p\u00e9riodes)", "title": "S\u00e9lection tarifaire" } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Puissance souscrite (kW)", + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", + "tariff": "Tarif applicable par zone g\u00e9ographique" + }, + "description": "Ce capteur utilise l'API officielle pour obtenir [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)](https://www.esios.ree.es/es/pvpc) en Espagne.\n Pour des explications plus pr\u00e9cises, visitez les [docs d'int\u00e9gration](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configuration du capteur" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/he.json b/homeassistant/components/pvpc_hourly_pricing/translations/he.json index 48a6eeeea33..951e9b21b2f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/he.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/he.json @@ -3,5 +3,12 @@ "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" } + }, + "options": { + "step": { + "init": { + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ca.json b/homeassistant/components/renault/translations/ca.json new file mode 100644 index 00000000000..8315d35b87b --- /dev/null +++ b/homeassistant/components/renault/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "kamereon_no_account": "No s'ha pogut trobar cap compte Kamereon." + }, + "error": { + "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID del compte Kamereon" + }, + "title": "Seleccioneu l'ID del compte Kamereon" + }, + "user": { + "data": { + "locale": "Llengua/regi\u00f3", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Defineix les credencials de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/cs.json b/homeassistant/components/renault/translations/cs.json new file mode 100644 index 00000000000..d731b4c2ec0 --- /dev/null +++ b/homeassistant/components/renault/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/de.json b/homeassistant/components/renault/translations/de.json new file mode 100644 index 00000000000..16650b8d63e --- /dev/null +++ b/homeassistant/components/renault/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "kamereon_no_account": "Kamereon-Konto kann nicht gefunden werden." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon-Kontonummer" + }, + "title": "Kamereon-Kontonummer ausw\u00e4hlen" + }, + "user": { + "data": { + "locale": "Gebietsschema", + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Renault-Anmeldeinformationen festlegen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/en.json b/homeassistant/components/renault/translations/en.json index bb65493a3b3..87186e6f59c 100644 --- a/homeassistant/components/renault/translations/en.json +++ b/homeassistant/components/renault/translations/en.json @@ -1,27 +1,27 @@ { - "config": { - "abort": { - "already_configured": "Account already configured", - "kamereon_no_account": "Unable to find Kamereon account." - }, - "error": { - "invalid_credentials": "Invalid credentials." - }, - "step": { - "kamereon": { - "data": { - "kamereon_account_id": "Kamereon account id" + "config": { + "abort": { + "already_configured": "Account is already configured", + "kamereon_no_account": "Unable to find Kamereon account." }, - "title": "Select Kamereon account id" - }, - "user": { - "data": { - "locale": "Locale", - "username": "Email", - "password": "Password" + "error": { + "invalid_credentials": "Invalid authentication" }, - "title": "Set Renault credentials" - } + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Select Kamereon account id" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Password", + "username": "Email" + }, + "title": "Set Renault credentials" + } + } } - } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/et.json b/homeassistant/components/renault/translations/et.json new file mode 100644 index 00000000000..bae0db1aed7 --- /dev/null +++ b/homeassistant/components/renault/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "kamereon_no_account": "Kamereoni kontot ei leitud." + }, + "error": { + "invalid_credentials": "Tuvastamine nurjus" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereoni konto ID" + }, + "title": "Vali Kamereoni konto ID" + }, + "user": { + "data": { + "locale": "Riigi kood (n\u00e4iteks EE)", + "password": "Salas\u00f5na", + "username": "E-posti aadress" + }, + "title": "M\u00e4\u00e4ra Renault sidumise parameetrid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json new file mode 100644 index 00000000000..874a9b8df67 --- /dev/null +++ b/homeassistant/components/renault/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "kamereon_no_account": "Impossible de trouver le compte Kamereon." + }, + "error": { + "invalid_credentials": "Authentification incorrecte" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Identifiant du compte Kamereon" + }, + "title": "S\u00e9lectionner l'identifiant du compte Kamereon" + }, + "user": { + "data": { + "locale": "Lieu", + "password": "Mot de passe", + "username": "Email" + }, + "title": "D\u00e9finir les informations d'identification de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json new file mode 100644 index 00000000000..d20e2d36a81 --- /dev/null +++ b/homeassistant/components/renault/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_credentials": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/it.json b/homeassistant/components/renault/translations/it.json new file mode 100644 index 00000000000..37ba94b3cdf --- /dev/null +++ b/homeassistant/components/renault/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "kamereon_no_account": "Impossibile trovare l'account Kamereon." + }, + "error": { + "invalid_credentials": "Autenticazione non valida" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID account Kamereon" + }, + "title": "Seleziona l'id dell'account Kamereon" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Password", + "username": "E-mail" + }, + "title": "Imposta le credenziali Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json new file mode 100644 index 00000000000..4840dd0c07b --- /dev/null +++ b/homeassistant/components/renault/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "kamereon_no_account": "Kan Kamereon-account niet vinden." + }, + "error": { + "invalid_credentials": "Ongeldige authenticatie" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon account id" + }, + "title": "Selecteer Kamereon-account-ID" + }, + "user": { + "data": { + "locale": "Locale", + "password": "Wachtwoord", + "username": "E-mail" + }, + "title": "Renault-inloggegevens instellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json new file mode 100644 index 00000000000..f367c8c540d --- /dev/null +++ b/homeassistant/components/renault/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-Post" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/pl.json b/homeassistant/components/renault/translations/pl.json new file mode 100644 index 00000000000..1d518cc14fb --- /dev/null +++ b/homeassistant/components/renault/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "kamereon_no_account": "Nie mo\u017cna znale\u017a\u0107 konta Kamereon." + }, + "error": { + "invalid_credentials": "Niepoprawne uwierzytelnienie" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Identyfikator konta Kamereon" + }, + "title": "Wyb\u00f3r identyfikatora konta Kamereon" + }, + "user": { + "data": { + "locale": "Ustawienia regionalne", + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Dane logowania Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/ru.json b/homeassistant/components/renault/translations/ru.json new file mode 100644 index 00000000000..822d42b6117 --- /dev/null +++ b/homeassistant/components/renault/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "kamereon_no_account": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Kamereon." + }, + "error": { + "invalid_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Kamereon" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 ID \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Kamereon" + }, + "user": { + "data": { + "locale": "\u0420\u0435\u0433\u0438\u043e\u043d", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/zh-Hant.json b/homeassistant/components/renault/translations/zh-Hant.json new file mode 100644 index 00000000000..4ae5413499d --- /dev/null +++ b/homeassistant/components/renault/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "kamereon_no_account": "\u627e\u4e0d\u5230 Kamereon \u5e33\u865f\u3002" + }, + "error": { + "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon \u5e33\u865f ID" + }, + "title": "\u9078\u64c7 Kamereon \u5e33\u865f ID" + }, + "user": { + "data": { + "locale": "\u4f4d\u7f6e", + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u8a2d\u5b9a Renault \u6191\u8b49" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json index 41d59c29fd8..12dc4bb482b 100644 --- a/homeassistant/components/roku/translations/he.json +++ b/homeassistant/components/roku/translations/he.json @@ -10,6 +10,14 @@ }, "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json index e2d0c48b0b9..4520671eedb 100644 --- a/homeassistant/components/roomba/translations/he.json +++ b/homeassistant/components/roomba/translations/he.json @@ -34,7 +34,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05db\u05e8\u05d2\u05e2 \u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4-BLID \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05ea\u05d4\u05dc\u05d9\u05da \u05d9\u05d3\u05e0\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea Roomba \u05d0\u05d5 Braava." } } } diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index 452436d0e05..df8fa80b4dd 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -16,7 +16,7 @@ "data": { "host": "Host" }, - "description": "Voer de hostnaam of het IP-adres van uw Roon-server in." + "description": "Kon de Roon-server niet vinden, voer de hostnaam of het IP-adres in." } } } diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 15a529c94b2..5a20992d8e5 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -5,7 +5,13 @@ "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. Veuillez v\u00e9rifier les param\u00e8tres de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant.", "cannot_connect": "\u00c9chec de connexion", - "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." + "id_missing": "Cet appareil Samsung n'a pas de num\u00e9ro de s\u00e9rie.", + "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", + "unknown": "Erreur inattendue" + }, + "error": { + "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. V\u00e9rifiez les param\u00e8tres du Gestionnaire de p\u00e9riph\u00e9riques externes de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant." }, "flow_title": "Samsung TV: {model}", "step": { @@ -13,6 +19,9 @@ "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", "title": "TV Samsung" }, + "reauth_confirm": { + "description": "Apr\u00e8s avoir soumis, acceptez la fen\u00eatre contextuelle sur {device} demandant l'autorisation dans les 30 secondes." + }, "user": { "data": { "host": "Nom d'h\u00f4te ou adresse IP", diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json index 84f425be218..30e3ee0c726 100644 --- a/homeassistant/components/screenlogic/translations/de.json +++ b/homeassistant/components/screenlogic/translations/de.json @@ -18,7 +18,7 @@ }, "gateway_select": { "data": { - "selected_gateway": "" + "selected_gateway": "Gateway" }, "description": "Die folgenden ScreenLogic-Gateways wurden erkannt. Bitte w\u00e4hle eines aus, um es zu konfigurieren oder w\u00e4hle ein ScreenLogic-Gateway zum manuellen Konfigurieren.", "title": "ScreenLogic" diff --git a/homeassistant/components/select/translations/fr.json b/homeassistant/components/select/translations/fr.json index 7a3633cb309..5248940b2c4 100644 --- a/homeassistant/components/select/translations/fr.json +++ b/homeassistant/components/select/translations/fr.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "Modifier l'option {entity_name}" + }, + "condition_type": { + "selected_option": "Option actuellement s\u00e9lectionn\u00e9e {entity_name}" + }, + "trigger_type": { + "current_option_changed": "Modification de l\u2019option {entity_name}" + } + }, "title": "S\u00e9lectionner" } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/fr.json b/homeassistant/components/sia/translations/fr.json new file mode 100644 index 00000000000..2b3188dd082 --- /dev/null +++ b/homeassistant/components/sia/translations/fr.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Le compte n'est pas une valeur hexad\u00e9cimale, veuillez utiliser uniquement 0-9 et AF.", + "invalid_account_length": "Le compte n'est pas de la bonne longueur, il doit faire entre 3 et 16 caract\u00e8res.", + "invalid_key_format": "La cl\u00e9 n'est pas une valeur hexad\u00e9cimale, veuillez utiliser uniquement 0-9 et AF.", + "invalid_key_length": "La cl\u00e9 n'est pas de la bonne longueur, elle doit comporter 16, 24 ou 32 caract\u00e8res hexad\u00e9cimaux.", + "invalid_ping": "L'intervalle de ping doit \u00eatre compris entre 1 et 1440 minutes.", + "invalid_zones": "Il doit y avoir au moins 1 zone.", + "unknown": "Erreur inattendue" + }, + "step": { + "additional_account": { + "data": { + "account": "Identifiant du compte", + "additional_account": "Comptes suppl\u00e9mentaires", + "encryption_key": "Cl\u00e9 de cryptage", + "ping_interval": "Intervalle de ping (min)", + "zones": "Nombre de zones pour le compte" + }, + "title": "Ajouter un autre compte au port actuel." + }, + "user": { + "data": { + "account": "Identifiant de compte", + "additional_account": "Comptes suppl\u00e9mentaires", + "encryption_key": "Cl\u00e9 de cryptage", + "ping_interval": "Intervalle de ping (min)", + "port": "Port", + "protocol": "Protocole", + "zones": "Nombre de zones pour le compte" + }, + "title": "Cr\u00e9er une connexion pour les syst\u00e8mes d'alarme bas\u00e9s sur SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorer la v\u00e9rification de l'horodatage des \u00e9v\u00e9nements SIA", + "zones": "Nombre de zones pour le compte" + }, + "description": "D\u00e9finisser les options du compte\u00a0: {account}", + "title": "Options pour la configuration SIA." + } + } + }, + "title": "Syst\u00e8mes d'alarme SIA" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 966f3598d95..ae354b2138a 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "mfa": { - "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", + "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", "title": "SimpliSafe Multi-Faktor-Authentifizierung" }, "reauth_confirm": { diff --git a/homeassistant/components/sma/translations/fr.json b/homeassistant/components/sma/translations/fr.json index ab154fea3f8..e70401c87f5 100644 --- a/homeassistant/components/sma/translations/fr.json +++ b/homeassistant/components/sma/translations/fr.json @@ -1,17 +1,23 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration est d\u00e9j\u00e0 en cours" }, "error": { - "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil" + "cannot_connect": "\u00c9chec de connexion", + "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" }, "step": { "user": { "data": { "group": "Groupe", "host": "H\u00f4te ", - "password": "Mot de passe" + "password": "Mot de passe", + "ssl": "Utilise un certificat SSL", + "verify_ssl": "V\u00e9rifier le certificat SSL" }, "description": "Saisissez les informations relatives \u00e0 votre appareil SMA.", "title": "Configurer SMA Solar" diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index c098bfafbee..db5bc91bf7b 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -19,7 +19,7 @@ } }, "user": { - "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", + "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "title": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d4\u05ea\u05e7\u05e9\u05e8\u05d5\u05ea \u05d7\u05d6\u05e8\u05d4" } } diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index c6f3fdb8ce3..c660ca15e87 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -9,7 +9,8 @@ }, "step": { "reauth_confirm": { - "description": "L'int\u00e9gration SmartTub doit r\u00e9-authentifier votre compte" + "description": "L'int\u00e9gration SmartTub doit r\u00e9-authentifier votre compte", + "title": "R\u00e9authentification de l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index d88d1320279..9fc1af6d92c 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -49,5 +49,5 @@ } } }, - "title": "" + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/fr.json b/homeassistant/components/sonos/translations/fr.json index 2bae0a69826..50a6086e2e8 100644 --- a/homeassistant/components/sonos/translations/fr.json +++ b/homeassistant/components/sonos/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Aucun p\u00e9riph\u00e9rique Sonos trouv\u00e9 sur le r\u00e9seau.", + "not_sonos_device": "L'appareil d\u00e9couvert n'est pas un appareil Sonos", "single_instance_allowed": "Une seule configuration de Sonos est n\u00e9cessaire." }, "step": { diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 91cbe81a2a6..878c14a5119 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 Sonos \u05d1\u05e8\u05e9\u05ea.", - "single_instance_allowed": "\u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Sonos \u05e0\u05d7\u05d5\u05e6\u05d4." + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "confirm": { diff --git a/homeassistant/components/switcher_kis/translations/fr.json b/homeassistant/components/switcher_kis/translations/fr.json index 059be87fc07..e6e7a3c271f 100644 --- a/homeassistant/components/switcher_kis/translations/fr.json +++ b/homeassistant/components/switcher_kis/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "step": { "confirm": { diff --git a/homeassistant/components/switcher_kis/translations/it.json b/homeassistant/components/switcher_kis/translations/it.json new file mode 100644 index 00000000000..0278fe07bfe --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/fr.json b/homeassistant/components/syncthing/translations/fr.json new file mode 100644 index 00000000000..12486fb5cf2 --- /dev/null +++ b/homeassistant/components/syncthing/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "user": { + "data": { + "title": "Configurer l'int\u00e9gration de Syncthing", + "token": "Jeton", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "title": "Synchroniser" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index a9fdd199618..561c6c97e0b 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -28,6 +29,13 @@ "description": "Chcete nastavit {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Synology DSM Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index ba4d0a88a6b..b254fc8e561 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9" + "already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -34,7 +35,8 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Raison: {details}" + "description": "Raison: {details}", + "title": "Synology DSM R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json index a671684a770..4d95d5c2c3c 100644 --- a/homeassistant/components/synology_dsm/translations/he.json +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -19,7 +20,13 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" }, - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" + }, + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 6f5fd4ac245..bb6965255bb 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -29,6 +30,14 @@ "description": "Vuoi impostare {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Motivo: {details}", + "title": "Synology DSM Autenticare nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index 187360bac5e..a21fab81777 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" }, "flow_title": "Pont syst\u00e8me: {name}", "step": { diff --git a/homeassistant/components/system_health/translations/he.json b/homeassistant/components/system_health/translations/he.json index 2c46fb48c7d..ae5bdc2388a 100644 --- a/homeassistant/components/system_health/translations/he.json +++ b/homeassistant/components/system_health/translations/he.json @@ -1,3 +1,3 @@ { - "title": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea" + "title": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea \u05d4\u05de\u05e2\u05e8\u05db\u05ea" } \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/ca.json b/homeassistant/components/tesla/translations/ca.json index 2a51c0297ae..f5c0117f6a0 100644 --- a/homeassistant/components/tesla/translations/ca.json +++ b/homeassistant/components/tesla/translations/ca.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Codi MFA (opcional)", "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index bdcd8237b3b..09934369f6b 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA-Code (optional)", "password": "Passwort", "username": "E-Mail" }, diff --git a/homeassistant/components/tesla/translations/et.json b/homeassistant/components/tesla/translations/et.json index c7ceae36990..ab36a4e503d 100644 --- a/homeassistant/components/tesla/translations/et.json +++ b/homeassistant/components/tesla/translations/et.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA kood (valikuline)", "password": "Salas\u00f5na", "username": "E-post" }, diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index 889c32a7d91..174b687f26f 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Code MFA (facultatif)", "password": "Mot de passe", "username": "Email" }, diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json index 3a137da78f1..05a663df149 100644 --- a/homeassistant/components/tesla/translations/it.json +++ b/homeassistant/components/tesla/translations/it.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Codice autenticazione a pi\u00f9 fattori MFA (facoltativo)", "password": "Password", "username": "E-mail" }, diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index 5655a641f96..689766cd906 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA Code (optioneel)", "password": "Wachtwoord", "username": "E-mail" }, diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json index 7ec634cd56c..266a0e82dbe 100644 --- a/homeassistant/components/tesla/translations/pl.json +++ b/homeassistant/components/tesla/translations/pl.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "Kod uwierzytelniania wielosk\u0142adnikowego (opcjonalnie)", "password": "Has\u0142o", "username": "Adres e-mail" }, diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json index d62a2e1f168..191d10b8bea 100644 --- a/homeassistant/components/tesla/translations/ru.json +++ b/homeassistant/components/tesla/translations/ru.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "\u041a\u043e\u0434 MFA (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, diff --git a/homeassistant/components/tesla/translations/zh-Hant.json b/homeassistant/components/tesla/translations/zh-Hant.json index d9b7fd4ef79..9ff407efaa3 100644 --- a/homeassistant/components/tesla/translations/zh-Hant.json +++ b/homeassistant/components/tesla/translations/zh-Hant.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA \u78bc\uff08\u9078\u9805\uff09", "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6" }, diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index b46bf127963..668b20726fc 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -11,7 +11,8 @@ "step": { "locations": { "data": { - "location": "Emplacement" + "location": "Emplacement", + "usercode": "Code d'utilisateur" }, "description": "Saisissez le code d'utilisateur de cet utilisateur \u00e0 cet emplacement", "title": "Codes d'utilisateur de l'emplacement" diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index 55d53f6e676..888c65226dc 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9 TP-Link \u05d1\u05e8\u05e9\u05ea.", - "single_instance_allowed": "\u05e0\u05d3\u05e8\u05e9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d1\u05dc\u05d1\u05d3" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "confirm": { diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json index 7e253c1d05f..3e94aaeb4c5 100644 --- a/homeassistant/components/traccar/translations/de.json +++ b/homeassistant/components/traccar/translations/de.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json index 83c34cb9c77..4fe52a3cf8b 100644 --- a/homeassistant/components/unifi/translations/he.json +++ b/homeassistant/components/unifi/translations/he.json @@ -33,6 +33,14 @@ "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)" } }, + "init": { + "data": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + } + }, "simple_options": { "data": { "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea", diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json index 86e4d7409cf..7c0454e7707 100644 --- a/homeassistant/components/upb/translations/de.json +++ b/homeassistant/components/upb/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", + "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index fe1f1366d39..ffbef69abe7 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -26,5 +26,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour (secondes, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index 706d87f0db4..e9aba0a7a58 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -12,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "init": { + "many": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, "user": { "data": { "unique_id": "\u05d4\u05ea\u05e7\u05df", diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json new file mode 100644 index 00000000000..04428ef567f --- /dev/null +++ b/homeassistant/components/wallbox/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification incorrecte", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "station": "Num\u00e9ro de s\u00e9rie de la station", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/fr.json b/homeassistant/components/wemo/translations/fr.json index ccf2ac6ef21..e0372147f58 100644 --- a/homeassistant/components/wemo/translations/fr.json +++ b/homeassistant/components/wemo/translations/fr.json @@ -9,5 +9,10 @@ "description": "Voulez-vous configurer Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Le bouton Wemo a \u00e9t\u00e9 enfonc\u00e9 pendant 2 secondes" + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json index 137decb7f40..dec038a8a92 100644 --- a/homeassistant/components/wled/translations/fr.json +++ b/homeassistant/components/wled/translations/fr.json @@ -20,5 +20,14 @@ "title": "Dispositif WLED d\u00e9couvert" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Garder la lumi\u00e8re principale, m\u00eame avec 1 segment LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 6b1baf8b0bf..497f559a9de 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -10,7 +10,7 @@ "at_abschaltung": "AT Abschaltung", "at_frostschutz": "AT Frostschutz", "aus": "Aus", - "auto": "", + "auto": "Automatisch", "auto_off_cool": "AutoOffCool", "auto_on_cool": "AutoOnCool", "automatik_aus": "Automatik AUS", diff --git a/homeassistant/components/wolflink/translations/sensor.he.json b/homeassistant/components/wolflink/translations/sensor.he.json index 68b635ba82b..8447fd66b31 100644 --- a/homeassistant/components/wolflink/translations/sensor.he.json +++ b/homeassistant/components/wolflink/translations/sensor.he.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "permanent": "\u05e7\u05d1\u05d5\u05e2", "solarbetrieb": "\u05de\u05e6\u05d1 \u05e1\u05d5\u05dc\u05d0\u05e8\u05d9", "standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4", "start": "\u05d4\u05ea\u05d7\u05dc", diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 23a003daa39..17363b347c0 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -13,7 +13,7 @@ "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.", "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", - "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." + "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 82b2d4991cb..2b68325a246 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -2,10 +2,16 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours." + "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours.", + "incomplete_info": "Informations incompl\u00e8tes pour configurer l'appareil, aucun h\u00f4te ou jeton fourni.", + "not_xiaomi_miio": "L'appareil n'est pas (encore) pris en charge par Xiaomi Miio.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { "cannot_connect": "\u00c9chec de connexion", + "cloud_credentials_incomplete": "Identifiants cloud incomplets, veuillez renseigner le nom d'utilisateur, le mot de passe et le pays", + "cloud_login_error": "Impossible de se connecter \u00e0 Xioami Miio Cloud, v\u00e9rifiez les informations d'identification.", + "cloud_no_devices": "Aucun appareil trouv\u00e9 dans ce compte cloud Xiaomi Miio.", "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.", "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration." }, @@ -13,14 +19,20 @@ "step": { "cloud": { "data": { + "cloud_country": "Pays du serveur cloud", "cloud_password": "Mot de passe cloud", - "cloud_username": "Nom d'utilisateur cloud" - } + "cloud_username": "Nom d'utilisateur cloud", + "manual": "Configurer manuellement (non recommand\u00e9)" + }, + "description": "Connectez-vous au cloud Xiaomi Miio, voir https://www.openhab.org/addons/bindings/miio/#country-servers pour le serveur cloud \u00e0 utiliser.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "connect": { "data": { "model": "Mod\u00e8le d'appareil" - } + }, + "description": "S\u00e9lectionner manuellement le mod\u00e8le d'appareil parmi les mod\u00e8les pris en charge.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "device": { "data": { @@ -41,10 +53,24 @@ "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", "title": "Se connecter \u00e0 la passerelle Xiaomi" }, + "manual": { + "data": { + "host": "Adresse IP", + "token": "Jeton API" + }, + "description": "Vous aurez besoin du jeton API de 32 caract\u00e8res, voir https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token pour les instructions. Veuillez noter que ce jeton API est diff\u00e9rent de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration de Xiaomi Miio doit r\u00e9-authentifier votre compte afin de mettre \u00e0 jour les jetons ou d'ajouter les informations d'identification cloud manquantes.", + "title": "R\u00e9authentification de l'int\u00e9gration" + }, "select": { "data": { "select_device": "Appareil Miio" - } + }, + "description": "S\u00e9lectionner l'appareil Xiaomi Miio \u00e0 configurer.", + "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 36f54e79361..e3bf59f9459 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -58,7 +58,7 @@ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, - "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "reauth_confirm": { diff --git a/homeassistant/components/yale_smart_alarm/translations/ca.json b/homeassistant/components/yale_smart_alarm/translations/ca.json index f3865103996..ab77170999b 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ca.json +++ b/homeassistant/components/yale_smart_alarm/translations/ca.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID d'\u00e0rea", + "name": "Nom", "password": "Contrasenya", "username": "Nom d'usuari" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID d'\u00e0rea", + "name": "Nom", "password": "Contrasenya", "username": "Nom d'usuari" } diff --git a/homeassistant/components/yale_smart_alarm/translations/cs.json b/homeassistant/components/yale_smart_alarm/translations/cs.json new file mode 100644 index 00000000000..f19158bca25 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", + "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/en.json b/homeassistant/components/yale_smart_alarm/translations/en.json index 84cfb893ad5..a439971fb3f 100644 --- a/homeassistant/components/yale_smart_alarm/translations/en.json +++ b/homeassistant/components/yale_smart_alarm/translations/en.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "Area ID", + "name": "Name", "password": "Password", "username": "Username" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "Area ID", + "name": "Name", "password": "Password", "username": "Username" } diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json new file mode 100644 index 00000000000..60d6f5cc548 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification incorrecte" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de la zone", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "user": { + "data": { + "area_id": "ID de la zone", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/he.json b/homeassistant/components/yale_smart_alarm/translations/he.json new file mode 100644 index 00000000000..41f5d4493bf --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "\u05de\u05d6\u05d4\u05d4 \u05d0\u05d6\u05d5\u05e8", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "area_id": "\u05de\u05d6\u05d4\u05d4 \u05d0\u05d6\u05d5\u05e8", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/it.json b/homeassistant/components/yale_smart_alarm/translations/it.json new file mode 100644 index 00000000000..2f510e46396 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID area", + "name": "Nome", + "password": "Password", + "username": "Nome utente" + } + }, + "user": { + "data": { + "area_id": "ID area", + "name": "Nome", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json new file mode 100644 index 00000000000..53c1b8fb086 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Area ID", + "name": "Naam", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, + "user": { + "data": { + "area_id": "Area ID", + "name": "Naam", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/no.json b/homeassistant/components/yale_smart_alarm/translations/no.json new file mode 100644 index 00000000000..bbeedb7dc89 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "name": "Navn", + "password": "Passord", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "name": "Navn", + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/pl.json b/homeassistant/components/yale_smart_alarm/translations/pl.json new file mode 100644 index 00000000000..553d05ee439 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Identyfikator obszaru", + "name": "Nazwa", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "area_id": "Identyfikator obszaru", + "name": "[%key::common::config_flow::data::name%]", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ru.json b/homeassistant/components/yale_smart_alarm/translations/ru.json index 03af44dd983..aedf07d030e 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ru.json +++ b/homeassistant/components/yale_smart_alarm/translations/ru.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "ID \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json index 6cfcaf7c83b..e02b74f27a1 100644 --- a/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hant.json @@ -9,16 +9,16 @@ "step": { "reauth_confirm": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "\u5206\u5340 ID", + "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } }, "user": { "data": { - "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]", - "name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]", + "area_id": "\u5206\u5340 ID", + "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } diff --git a/homeassistant/components/yamaha_musiccast/translations/fr.json b/homeassistant/components/yamaha_musiccast/translations/fr.json new file mode 100644 index 00000000000..0a8671dc2aa --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "yxc_control_url_missing": "L'URL de contr\u00f4le n'est pas donn\u00e9e dans la description ssdp." + }, + "error": { + "no_musiccast_device": "Cet appareil ne semble pas \u00eatre un appareil MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurer MusicCast pour l'int\u00e9grer \u00e0 Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index be55fe57ee0..9682f0d9b6f 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer {model} ({host})\u00a0?" + }, "pick_device": { "data": { "device": "Appareil" diff --git a/homeassistant/components/youless/translations/ca.json b/homeassistant/components/youless/translations/ca.json new file mode 100644 index 00000000000..1237597b797 --- /dev/null +++ b/homeassistant/components/youless/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/cs.json b/homeassistant/components/youless/translations/cs.json new file mode 100644 index 00000000000..7a27355056b --- /dev/null +++ b/homeassistant/components/youless/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/de.json b/homeassistant/components/youless/translations/de.json new file mode 100644 index 00000000000..a87bbe1aa46 --- /dev/null +++ b/homeassistant/components/youless/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/en.json b/homeassistant/components/youless/translations/en.json index 38923682b10..584a8283675 100644 --- a/homeassistant/components/youless/translations/en.json +++ b/homeassistant/components/youless/translations/en.json @@ -1,21 +1,15 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, "step": { "user": { "data": { "host": "Host", - "password": "Password", - "username": "Username" + "name": "Name" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/et.json b/homeassistant/components/youless/translations/et.json new file mode 100644 index 00000000000..9a26513c333 --- /dev/null +++ b/homeassistant/components/youless/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/fr.json b/homeassistant/components/youless/translations/fr.json new file mode 100644 index 00000000000..6f9c76d9ba1 --- /dev/null +++ b/homeassistant/components/youless/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/he.json b/homeassistant/components/youless/translations/he.json new file mode 100644 index 00000000000..33660936e12 --- /dev/null +++ b/homeassistant/components/youless/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/it.json b/homeassistant/components/youless/translations/it.json new file mode 100644 index 00000000000..8f93107a15d --- /dev/null +++ b/homeassistant/components/youless/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/nl.json b/homeassistant/components/youless/translations/nl.json new file mode 100644 index 00000000000..a05a1e161cc --- /dev/null +++ b/homeassistant/components/youless/translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/no.json b/homeassistant/components/youless/translations/no.json new file mode 100644 index 00000000000..01ea5b65fb1 --- /dev/null +++ b/homeassistant/components/youless/translations/no.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Navn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/pl.json b/homeassistant/components/youless/translations/pl.json new file mode 100644 index 00000000000..98acbf5ef4b --- /dev/null +++ b/homeassistant/components/youless/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/ru.json b/homeassistant/components/youless/translations/ru.json new file mode 100644 index 00000000000..341b6a603aa --- /dev/null +++ b/homeassistant/components/youless/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/zh-Hant.json b/homeassistant/components/youless/translations/zh-Hant.json new file mode 100644 index 00000000000..9ba777cefba --- /dev/null +++ b/homeassistant/components/youless/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 75ba26ca809..90d0908d6c3 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -41,6 +41,8 @@ "title": "Options du panneau de contr\u00f4le d'alarme" }, "zha_options": { + "consider_unavailable_battery": "Consid\u00e9rer les appareils aliment\u00e9s par batterie indisponibles apr\u00e8s (secondes)", + "consider_unavailable_mains": "Consid\u00e9rer les appareils aliment\u00e9s par le secteur indisponibles apr\u00e8s (secondes)", "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", "title": "Options g\u00e9n\u00e9rales" diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index b226a2e51e0..9d432edd1e5 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -13,7 +13,7 @@ "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", "usb_path": "USB-Ger\u00e4te-Pfad" }, - "description": "Diese Integration wird nicht mehr gepflegt. Verwenden Sie bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" + "description": "Diese Integration wird nicht mehr gepflegt. Verwende bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" } } }, diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json index 585b696b496..9cbf39a6d16 100644 --- a/homeassistant/components/zwave/translations/he.json +++ b/homeassistant/components/zwave/translations/he.json @@ -15,13 +15,13 @@ "state": { "_": { "dead": "\u05de\u05ea", - "initializing": "\u05de\u05d0\u05ea\u05d7\u05dc", + "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc", "ready": "\u05de\u05d5\u05db\u05df", "sleeping": "\u05d9\u05e9\u05df" }, "query_stage": { - "dead": "\u05de\u05ea ({query_stage})", - "initializing": "\u05de\u05d0\u05ea\u05d7\u05dc ({query_stage})" + "dead": "\u05de\u05ea", + "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 6435453b1df..9b01865d3be 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -116,5 +116,5 @@ } } }, - "title": "" + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 46737b8c79d..1e51e97044a 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -52,23 +52,67 @@ } }, "device_automation": { + "condition_type": { + "config_parameter": "Valeur du param\u00e8tre de configuration {subtype}", + "node_status": "\u00c9tat du n\u0153ud", + "value": "Valeur actuelle d'une valeur Z-Wave" + }, "trigger_type": { + "event.notification.entry_control": "Envoi d'une notification de contr\u00f4le d'entr\u00e9e", "event.notification.notification": "Envoyer une notification", + "event.value_notification.basic": "\u00c9v\u00e9nement CC de base sur {subtype}", + "event.value_notification.central_scene": "Action de la sc\u00e8ne centrale sur {subtype}", "event.value_notification.scene_activation": "Activation de la sc\u00e8ne sur {sous-type}", "state.node_status": "Changement de statut du noeud" } }, "options": { + "abort": { + "addon_get_discovery_info_failed": "\u00c9chec de l'obtention des informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", + "addon_info_failed": "\u00c9chec de l'obtention des informations sur le module compl\u00e9mentaire Z-Wave JS.", + "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", + "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", + "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez plut\u00f4t cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." + }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide" + "invalid_ws_url": "URL websocket invalide", + "unknown": "Erreur inattendue" + }, + "progress": { + "install_addon": "Veuillez patienter pendant que l'installation du module compl\u00e9mentaire Z-Wave JS se termine. Cela peut prendre plusieurs minutes.", + "start_addon": "Veuillez patienter pendant que le d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS se termine. Cela peut prendre quelques secondes." }, "step": { "configure_addon": { "data": { + "emulate_hardware": "\u00c9muler le mat\u00e9riel", "log_level": "Niveau du journal", - "network_key": "Cl\u00e9 r\u00e9seau" + "network_key": "Cl\u00e9 r\u00e9seau", + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "title": "Entrer dans la configuration du module compl\u00e9mentaire Z-Wave JS" + }, + "install_addon": { + "title": "L'installation du module compl\u00e9mentaire Z-Wave JS a commenc\u00e9" + }, + "manual": { + "data": { + "url": "URL" } + }, + "on_supervisor": { + "data": { + "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor" + }, + "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor\u00a0?", + "title": "S\u00e9lectionner la m\u00e9thode de connexion" + }, + "start_addon": { + "title": "Le module compl\u00e9mentaire Z-Wave JS d\u00e9marre." } } }, From 9cee9d9d8aa7c2e5c7316cb1f80374a72a725525 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 1 Aug 2021 20:35:03 -0700 Subject: [PATCH 070/199] Add energy consumption sensors to smartthings devices (#53759) --- .../components/smartthings/sensor.py | 62 +++++++++++++++++++ tests/components/smartthings/test_sensor.py | 49 +++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 7a7f9a51855..5059bcc4403 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -220,6 +220,7 @@ CAPABILITY_TO_SENSORS = { Capability.oven_setpoint: [ Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None) ], + Capability.power_consumption_report: [], Capability.power_meter: [ Map( Attribute.power, @@ -388,6 +389,13 @@ CAPABILITY_TO_SENSORS = { UNITS = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} THREE_AXIS_NAMES = ["X Coordinate", "Y Coordinate", "Z Coordinate"] +POWER_CONSUMPTION_REPORT_NAMES = [ + "energy", + "power", + "deltaEnergy", + "powerEnergy", + "energySaved", +] async def async_setup_entry(hass, config_entry, async_add_entities): @@ -403,6 +411,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for index in range(len(THREE_AXIS_NAMES)) ] ) + elif capability == Capability.power_consumption_report: + sensors.extend( + [ + SmartThingsPowerConsumptionSensor(device, report_name) + for report_name in POWER_CONSUMPTION_REPORT_NAMES + ] + ) else: maps = CAPABILITY_TO_SENSORS[capability] sensors.extend( @@ -526,3 +541,50 @@ class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): return three_axis[self._index] except (TypeError, IndexError): return None + + +class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): + """Define a SmartThings Sensor.""" + + def __init__( + self, + device: DeviceEntity, + report_name: str, + ) -> None: + """Init the class.""" + super().__init__(device) + self.report_name = report_name + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return f"{self._device.label} {self.report_name}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device.device_id}.{self.report_name}" + + @property + def state(self): + """Return the state of the sensor.""" + value = self._device.status.attributes[Attribute.power_consumption].value + if value.get(self.report_name) is None: + return None + if self.report_name == "power": + return value[self.report_name] + return value[self.report_name] / 1000 + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.report_name == "power": + return DEVICE_CLASS_POWER + return DEVICE_CLASS_ENERGY + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + if self.report_name == "power": + return POWER_WATT + return ENERGY_KILO_WATT_HOUR diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index ffb577c903a..fa849a3cc67 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -139,6 +139,55 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): assert entry.manufacturer == "Unavailable" +async def test_power_consumption_sensor(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "refrigerator", + [Capability.power_consumption_report], + { + Attribute.power_consumption: { + "energy": 1412002, + "deltaEnergy": 25, + "power": 109, + "powerEnergy": 24.304498331745464, + "persistedEnergy": 0, + "energySaved": 0, + "start": "2021-07-30T16:45:25Z", + "end": "2021-07-30T16:58:33Z", + } + }, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.refrigerator_energy") + assert state + assert state.state == "1412.002" + entry = entity_registry.async_get("sensor.refrigerator_energy") + assert entry + assert entry.unique_id == f"{device.device_id}.energy" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + state = hass.states.get("sensor.refrigerator_power") + assert state + assert state.state == "109" + entry = entity_registry.async_get("sensor.refrigerator_power") + assert entry + assert entry.unique_id == f"{device.device_id}.power" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" # Arrange From 3d6ba793f73f3eabe681f445236f929fd25fd4f2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 2 Aug 2021 04:59:32 +0100 Subject: [PATCH 071/199] Fix error in homekit_controller causing some entities to get an incorrect unique id (#53848) --- .../components/homekit_controller/__init__.py | 7 +- .../components/homekit_controller/number.py | 3 +- .../components/homekit_controller/sensor.py | 3 +- .../specific_devices/test_ecobee3.py | 2 +- .../specific_devices/test_koogeek_p1eu.py | 2 +- .../specific_devices/test_koogeek_sw2.py | 2 +- .../specific_devices/test_mysa_living.py | 91 +++++++ .../test_vocolinc_flowerbud.py | 4 +- .../homekit_controller/mysa_living.json | 250 ++++++++++++++++++ 9 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_mysa_living.py create mode 100644 tests/fixtures/homekit_controller/mysa_living.json diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 404b2f54ab0..c14cfbb8a7e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -189,11 +189,16 @@ class CharacteristicEntity(HomeKitEntity): the service entity. """ + def __init__(self, accessory, devinfo, char): + """Initialise a generic single characteristic HomeKit entity.""" + self._char = char + super().__init__(accessory, devinfo) + @property def unique_id(self) -> str: """Return the ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}-sid:{self._iid}-cid:{self._iid}" + return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 2e0193fa080..73d8cd6adbd 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -53,9 +53,8 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self._device_class = device_class self._icon = icon self._name = name - self._char = char - super().__init__(conn, info) + super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index f34fc3f0a9b..6bf8a7fc084 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -254,9 +254,8 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): self._unit = unit self._icon = icon self._name = name - self._char = char - super().__init__(conn, info) + super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 96dbd3b0718..94f3aabc12a 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -60,7 +60,7 @@ async def test_ecobee3_setup(hass): assert climate_state.attributes["max_humidity"] == 50 climate_sensor = entity_registry.async_get("sensor.homew_current_temperature") - assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:16" + assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:19" occ1 = entity_registry.async_get("binary_sensor.kitchen") assert occ1.unique_id == "homekit-AB1C-56" diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index db72aad7541..6302013223d 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -37,7 +37,7 @@ async def test_koogeek_p1eu_setup(hass): # Assert the power sensor is detected entry = entity_registry.async_get("sensor.koogeek_p1_a00aa0_real_time_energy") - assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:21" + assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:22" helper = Helper( hass, diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 00057822071..1d46b633153 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -45,7 +45,7 @@ async def test_koogeek_ls1_setup(hass): # Assert that the power sensor entity is correctly added to the entity registry entry = entity_registry.async_get("sensor.koogeek_sw2_187a91_real_time_energy") - assert entry.unique_id == "homekit-CNNT061751001372-aid:1-sid:14-cid:14" + assert entry.unique_id == "homekit-CNNT061751001372-aid:1-sid:14-cid:18" helper = Helper( hass, diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py new file mode 100644 index 00000000000..ea1c1084071 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -0,0 +1,91 @@ +"""Make sure that Mysa Living is enumerated properly.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_mysa_living_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mysa_living.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("sensor.mysa_85dda9_current_humidity") + assert entry.unique_id == "homekit-AAAAAAA000-aid:1-sid:20-cid:27" + + helper = Helper( + hass, + "sensor.mysa_85dda9_current_humidity", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9 - Current Humidity" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Empowered Homes Inc." + assert device.name == "Mysa-85dda9" + assert device.model == "v1" + assert device.sw_version == "2.8.1" + assert device.via_device_id is None + + # Assert the humidifier is detected + entry = entity_registry.async_get("sensor.mysa_85dda9_current_temperature") + assert entry.unique_id == "homekit-AAAAAAA000-aid:1-sid:20-cid:25" + + helper = Helper( + hass, + "sensor.mysa_85dda9_current_temperature", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9 - Current Temperature" + + # The sensor should be part of the same device + assert entry.device_id == device.id + + # Assert the light is detected + entry = entity_registry.async_get("light.mysa_85dda9") + assert entry.unique_id == "homekit-AAAAAAA000-40" + + helper = Helper( + hass, + "light.mysa_85dda9", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9" + + # The light should be part of the same device + assert entry.device_id == device.id + + # Assert the climate entity is detected + entry = entity_registry.async_get("climate.mysa_85dda9") + assert entry.unique_id == "homekit-AAAAAAA000-20" + + helper = Helper( + hass, + "climate.mysa_85dda9", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Mysa-85dda9" + + # The light should be part of the same device + assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index e2762b5c153..6968c62257f 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -20,7 +20,7 @@ async def test_vocolinc_flowerbud_setup(hass): # Check that the switch entity is handled correctly entry = entity_registry.async_get("number.vocolinc_flowerbud_0d324b") - assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:38" helper = Helper( hass, "number.vocolinc_flowerbud_0d324b", pairing, accessories[0], config_entry @@ -73,7 +73,7 @@ async def test_vocolinc_flowerbud_setup(hass): entry = entity_registry.async_get( "sensor.vocolinc_flowerbud_0d324b_current_humidity" ) - assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:30" + assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:33" helper = Helper( hass, diff --git a/tests/fixtures/homekit_controller/mysa_living.json b/tests/fixtures/homekit_controller/mysa_living.json new file mode 100644 index 00000000000..da26b654fe5 --- /dev/null +++ b/tests/fixtures/homekit_controller/mysa_living.json @@ -0,0 +1,250 @@ +[ + { + "aid": 1, + "primary": true, + "services": [ + { + "type": "0000004A-0000-1000-8000-0026BB765291", + "primary": true, + "iid": 20, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Thermostat", + "perms": [ + "pr" + ], + "iid": 24 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "format": "float", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 40, + "iid": 27, + "unit": "percentage", + "perms": [ + "pr", + "ev" + ] + }, + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 2, + "stepValue": 1, + "format": "uint8", + "perms": [ + "pr", + "ev" + ], + "iid": 21 + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 3, + "stepValue": 1, + "format": "uint8", + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 22 + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "value": 24.1, + "minValue": 0, + "maxValue": 100, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": [ + "pr", + "ev" + ], + "iid": 25 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "value": 22, + "minValue": 5, + "maxValue": 30, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 23 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "format": "uint8", + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "value": 0, + "iid": 26, + "perms": [ + "pr", + "pw", + "ev" + ] + } + ] + }, + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "iid": 2, + "format": "bool" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Empowered Homes Inc.", + "perms": [ + "pr" + ], + "iid": 3 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "v1", + "perms": [ + "pr" + ], + "iid": 4 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Mysa-85dda9", + "perms": [ + "pr" + ], + "iid": 5 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "AAAAAAA000", + "perms": [ + "pr" + ], + "iid": 6 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.8.1", + "perms": [ + "pr" + ], + "iid": 7 + }, + { + "hidden": true, + "type": "22280E2C-9B79-43BD-8370-5A8F67777B29", + "format": "string", + "value": "b4e62d85dda9", + "perms": [ + "pr" + ], + "iid": 8 + } + ] + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 10, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "iid": 11 + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 40, + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "iid": 42 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Display", + "perms": [ + "pr" + ], + "iid": 41 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "format": "int", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 0, + "iid": 43, + "unit": "percentage", + "perms": [ + "pr", + "pw", + "ev" + ] + } + ] + }, + { + "type": "3354EC82-AF38-4755-B4A4-4DB8E418F555", + "iid": 50, + "characteristics": [ + { + "hidden": true, + "type": "E71D8348-BB33-4C34-8C50-A64B1136EDD2", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw" + ], + "iid": 51 + } + ] + } + ] + } +] \ No newline at end of file From ab4ed128cc7f1f9bb815c6f8a273efff215d2bc2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Aug 2021 21:00:14 -0700 Subject: [PATCH 072/199] Bumped version to 2021.8.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 974fcd01b7b..0afc18f43bb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 1c30967f6fd3eedc86154f6bd0bb1d60bad88ffb Mon Sep 17 00:00:00 2001 From: Vinny Furia Date: Mon, 2 Aug 2021 06:57:10 -0600 Subject: [PATCH 073/199] Fix Radiothermostat hold value updates (#53656) --- homeassistant/components/radiotherm/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index aad6bf3989e..fc108af56a7 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -80,6 +80,8 @@ PRESET_MODE_TO_CODE = {"home": 0, "alternate": 1, "away": 2, "holiday": 3} CODE_TO_PRESET_MODE = {0: "home", 1: "alternate", 2: "away", 3: "holiday"} +CODE_TO_HOLD_STATE = {0: False, 1: True} + def round_temp(temperature): """Round a temperature to the resolution of the thermostat. @@ -300,6 +302,7 @@ class RadioThermostat(ClimateEntity): self._fstate = CODE_TO_FAN_STATE[data["fstate"]] self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] + self._hold_set = CODE_TO_HOLD_STATE[data["hold"]] self._current_operation = self._tmode if self._tmode == HVAC_MODE_COOL: From 1a1efecdba149899a19999205454b41ac687f806 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Mon, 2 Aug 2021 14:54:33 +0200 Subject: [PATCH 074/199] Fix missing default reconnect interval in dsmr (#53760) --- homeassistant/components/dsmr/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 5afc229a727..faff62ddeb4 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -157,7 +157,9 @@ async def async_setup_entry( update_entities_telegram({}) # throttle reconnect attempts - await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) + await asyncio.sleep( + entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) + ) except (serial.serialutil.SerialException, OSError): # Log any error while establishing connection and drop to retry @@ -167,7 +169,9 @@ async def async_setup_entry( protocol = None # throttle reconnect attempts - await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) + await asyncio.sleep( + entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) + ) except CancelledError: if stop_listener: stop_listener() # pylint: disable=not-callable From 91af3b0502618e518ec08fae286f0a06ca74f288 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 2 Aug 2021 09:59:23 -0300 Subject: [PATCH 075/199] Fix entry setup for Broadlink SP4 sensors (#53765) --- homeassistant/components/broadlink/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index f73f669326d..30bc8047d03 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -69,7 +69,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [ BroadlinkSensor(device, monitored_condition) for monitored_condition in sensor_data - if sensor_data[monitored_condition] != 0 or device.api.type == "A1" + if monitored_condition in SENSOR_TYPES + and ( + # These devices have optional sensors. + # We don't create entities if the value is 0. + sensor_data[monitored_condition] != 0 + or device.api.type not in {"RM4PRO", "RM4MINI"} + ) ] async_add_entities(sensors) From 31af17f7f741493c54783d5d4a5142da2f3e1d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 2 Aug 2021 11:14:45 +0200 Subject: [PATCH 076/199] Bump pysma to 0.6.5 (#53792) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 985a0506574..a462d0c854b 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.4"], + "requirements": ["pysma==0.6.5"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 107a538550c..7453a3cf2c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1760,7 +1760,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.4 +pysma==0.6.5 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea0e298a96b..04c093ad9a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1002,7 +1002,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.4 +pysma==0.6.5 # homeassistant.components.smappee pysmappee==0.2.25 From 2e441d8b7ca8d14e3cdfcec9c143a2c65eab9871 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Aug 2021 18:47:54 +0200 Subject: [PATCH 077/199] Fix TP-Link smart strip devices (#53799) --- homeassistant/components/tplink/__init__.py | 4 +- homeassistant/components/tplink/sensor.py | 4 +- homeassistant/components/tplink/switch.py | 2 +- tests/components/tplink/consts.py | 55 ++++++++++++++------- tests/components/tplink/test_init.py | 23 +++------ 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 88160722669..552e5666db8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -180,9 +180,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: continue hass_data[COORDINATORS][ - switch.mac + switch.context or switch.mac ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) - await coordinator.async_config_entry_first_refresh() if unavailable_devices: @@ -275,4 +274,5 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): except SmartDeviceException as ex: raise UpdateFailed(ex) from ex + self.name = data[CONF_ALIAS] return data diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index bb6596b82d1..697641915f7 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -101,7 +101,9 @@ async def async_setup_entry( ] switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] for switch in switches: - coordinator: SmartPlugDataUpdateCoordinator = coordinators[switch.mac] + coordinator: SmartPlugDataUpdateCoordinator = coordinators[ + switch.context or switch.mac + ] if not switch.has_emeter and coordinator.data.get(CONF_EMETER_PARAMS) is None: continue for description in ENERGY_SENSORS: diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index f5319de999a..10cf5c64d75 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -39,7 +39,7 @@ async def async_setup_entry( ] switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH] for switch in switches: - coordinator = coordinators[switch.mac] + coordinator = coordinators[switch.context or switch.mac] entities.append(SmartPlugSwitch(switch, coordinator)) async_add_entities(entities) diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py index 95177a12a9c..e579be61df2 100644 --- a/tests/components/tplink/consts.py +++ b/tests/components/tplink/consts.py @@ -61,31 +61,48 @@ SMARTPLUG_HS100_DATA = { "err_code": 0, } } -SMARTSTRIPWITCH_DATA = { +SMARTSTRIP_KP303_DATA = { "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS110(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", + "sw_ver": "1.0.4 Build 210428 Rel.135415", + "hw_ver": "1.0", + "model": "KP303(AU)", + "deviceId": "03102547AB1A57A4E4AA5B4EFE34C3005726B97D", + "oemId": "1F950FC9BFF278D9D35E046C129D9411", + "hwId": "9E86D4F840D2787D3D7A6523A731BA2C", + "rssi": -74, + "longitude_i": 1158985, + "latitude_i": -319172, + "alias": "TP-LINK_Power Strip_00B1", "status": "new", "mic_type": "IOT.SMARTPLUGSWITCH", "feature": "TIM", - "mac": "69:F2:3C:8E:E3:47", + "mac": "D4:DD:D6:95:B0:F9", "updating": 0, "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", - "next_action": {"type": -1}, - "children": [{"id": "1", "state": 1, "alias": "SmartPlug#1"}], + "children": [ + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913000", + "state": 0, + "alias": "R-Plug 1", + "on_time": 0, + "next_action": {"type": -1}, + }, + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913001", + "state": 1, + "alias": "R-Plug 2", + "on_time": 93835, + "next_action": {"type": -1}, + }, + { + "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913002", + "state": 1, + "alias": "R-Plug 3", + "on_time": 93834, + "next_action": {"type": -1}, + }, + ], + "child_num": 3, "err_code": 0, }, "realtime": { diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index a201788f35b..d96d6846939 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -20,11 +20,10 @@ from homeassistant.components.tplink.const import ( CONF_LIGHT, CONF_SW_VERSION, CONF_SWITCH, - COORDINATORS, UNAVAILABLE_RETRY_DELAY, ) from homeassistant.components.tplink.sensor import ENERGY_SENSORS -from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -34,7 +33,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro from tests.components.tplink.consts import ( SMARTPLUG_HS100_DATA, SMARTPLUG_HS110_DATA, - SMARTSTRIPWITCH_DATA, + SMARTSTRIP_KP303_DATA, ) @@ -307,7 +306,7 @@ async def test_smartstrip_device(hass: HomeAssistant): """Moked SmartStrip class.""" def get_sysinfo(self): - return SMARTSTRIPWITCH_DATA["sysinfo"] + return SMARTSTRIP_KP303_DATA["sysinfo"] with patch( "homeassistant.components.tplink.common.Discover.discover" @@ -315,9 +314,7 @@ async def test_smartstrip_device(hass: HomeAssistant): "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.common.SmartPlug.get_sysinfo", - return_value=SMARTSTRIPWITCH_DATA["sysinfo"], - ), patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + return_value=SMARTSTRIP_KP303_DATA["sysinfo"], ): strip = SmartStrip("123.123.123.123") @@ -326,16 +323,8 @@ async def test_smartstrip_device(hass: HomeAssistant): assert await async_setup_component(hass, tplink.DOMAIN, config) await hass.async_block_till_done() - assert hass.data.get(tplink.DOMAIN) - assert hass.data[tplink.DOMAIN].get(COORDINATORS) - assert hass.data[tplink.DOMAIN][COORDINATORS].get(strip.mac) - assert isinstance( - hass.data[tplink.DOMAIN][COORDINATORS][strip.mac], - tplink.SmartPlugDataUpdateCoordinator, - ) - data = hass.data[tplink.DOMAIN][COORDINATORS][strip.mac].data - assert data[CONF_ALIAS] == strip.sys_info["children"][0]["alias"] - assert data[CONF_DEVICE_ID] == "1" + entities = hass.states.async_entity_ids(SWITCH_DOMAIN) + assert len(entities) == 3 async def test_no_config_creates_no_entry(hass): From 5f8f1ae695467aa40654a9c8292b16013047a797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 2 Aug 2021 11:50:52 +0200 Subject: [PATCH 078/199] Add STATE_CLASS_MEASUREMENT to Tibber (#53802) --- homeassistant/components/tibber/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 3ee1a3749d1..b5012cdc41d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -77,12 +77,14 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="power", name="power", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), "powerProduction": TibberSensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=POWER_WATT, ), "minPower": TibberSensorEntityDescription( From e9b672c0b4a95b0314238cd87dc12affa1ecd2b3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Aug 2021 16:13:54 +0200 Subject: [PATCH 079/199] Fix crash when AVM FRITZ!SmartHome devices are unreachable (#53809) --- homeassistant/components/fritzbox/__init__.py | 5 +++++ homeassistant/components/fritzbox/binary_sensor.py | 2 -- homeassistant/components/fritzbox/climate.py | 5 ----- homeassistant/components/fritzbox/sensor.py | 8 ++++++-- homeassistant/components/fritzbox/switch.py | 5 ----- tests/components/fritzbox/test_binary_sensor.py | 4 ++-- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 087faeb2be9..cef325a61f3 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -143,6 +143,11 @@ class FritzBoxEntity(CoordinatorEntity): self._device_class = entity_info[ATTR_DEVICE_CLASS] self._attr_state_class = entity_info[ATTR_STATE_CLASS] + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.device.present + @property def device(self) -> FritzhomeDevice: """Return device object from coordinator.""" diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index f6dbaed97cf..5514408cb3c 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -54,6 +54,4 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - if not self.device.present: - return False return self.device.alert_state # type: ignore [no-any-return] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 0551c5e0455..4baa1b3b81a 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -93,11 +93,6 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """Return the list of supported features.""" return SUPPORT_FLAGS - @property - def available(self) -> bool: - """Return if thermostat is available.""" - return self.device.present # type: ignore [no-any-return] - @property def temperature_unit(self) -> str: """Return the unit of measurement that is used.""" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index d325e592faf..9d78afca4de 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -121,7 +121,9 @@ class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): @property def state(self) -> float | None: """Return the state of the sensor.""" - return self.device.power / 1000 # type: ignore [no-any-return] + if power := self.device.power: + return power / 1000 # type: ignore [no-any-return] + return 0.0 class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): @@ -130,7 +132,9 @@ class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): @property def state(self) -> float | None: """Return the state of the sensor.""" - return (self.device.energy or 0.0) / 1000 + if energy := self.device.energy: + return energy / 1000 # type: ignore [no-any-return] + return 0.0 @property def last_reset(self) -> datetime: diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 62d1cb1ecf8..133db92feda 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -56,11 +56,6 @@ async def async_setup_entry( class FritzboxSwitch(FritzBoxEntity, SwitchEntity): """The switch class for FRITZ!SmartHome switches.""" - @property - def available(self) -> bool: - """Return if switch is available.""" - return self.device.present # type: ignore [no-any-return] - @property def is_on(self) -> bool: """Return true if the switch is on.""" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index f4e32fbe3df..cb76109e0ff 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -14,8 +14,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -60,7 +60,7 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state - assert state.state == STATE_OFF + assert state.state == STATE_UNAVAILABLE async def test_update(hass: HomeAssistant, fritz: Mock): From d56636ed370f45b416e73b164d32daa4b2ef8926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 2 Aug 2021 18:46:07 +0200 Subject: [PATCH 080/199] Add base energy analytics (#53855) --- .../components/analytics/analytics.py | 11 +++ homeassistant/components/analytics/const.py | 2 + .../components/analytics/manifest.json | 15 +++- homeassistant/components/energy/__init__.py | 7 ++ tests/components/analytics/test_analytics.py | 69 +++++++++++++++++++ tests/components/energy/test_websocket_api.py | 6 +- 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 42630ad2df1..37aff988162 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,6 +8,10 @@ import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.energy import ( + DOMAIN as ENERGY_DOMAIN, + is_configured as energy_is_configured, +) from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -26,8 +30,10 @@ from .const import ( ATTR_AUTOMATION_COUNT, ATTR_BASE, ATTR_BOARD, + ATTR_CONFIGURED, ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, + ATTR_ENERGY, ATTR_HEALTHY, ATTR_INTEGRATION_COUNT, ATTR_INTEGRATIONS, @@ -222,6 +228,11 @@ class Analytics: if supervisor_info is not None: payload[ATTR_ADDONS] = addons + if ENERGY_DOMAIN in integrations: + payload[ATTR_ENERGY] = { + ATTR_CONFIGURED: await energy_is_configured(self.hass) + } + if self.preferences.get(ATTR_STATISTICS, False): payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all()) payload[ATTR_AUTOMATION_COUNT] = len( diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 4688c578a00..8576e22073f 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -21,8 +21,10 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" ATTR_BOARD = "board" +ATTR_CONFIGURED = "configured" ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" +ATTR_ENERGY = "energy" ATTR_HEALTHY = "healthy" ATTR_INSTALLATION_TYPE = "installation_type" ATTR_INTEGRATION_COUNT = "integration_count" diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index 49edf1bcf8c..2dae8d4e629 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -2,8 +2,17 @@ "domain": "analytics", "name": "Analytics", "documentation": "https://www.home-assistant.io/integrations/analytics", - "codeowners": ["@home-assistant/core", "@ludeeus"], - "dependencies": ["api", "websocket_api"], + "codeowners": [ + "@home-assistant/core", + "@ludeeus" + ], + "dependencies": [ + "api", + "websocket_api" + ], + "after_dependencies": [ + "energy" + ], "quality_scale": "internal", "iot_class": "cloud_push" -} +} \ No newline at end of file diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py index 1e060c1f35b..c856ffb1541 100644 --- a/homeassistant/components/energy/__init__.py +++ b/homeassistant/components/energy/__init__.py @@ -8,6 +8,13 @@ from homeassistant.helpers.typing import ConfigType from . import websocket_api from .const import DOMAIN +from .data import async_get_manager + + +async def is_configured(hass: HomeAssistant) -> bool: + """Return a boolean to indicate if energy is configured.""" + manager = await async_get_manager(hass) + return bool(manager.data != manager.default_preferences()) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ee67a7e3935..a781cb4c662 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -424,3 +424,72 @@ async def test_nightly_endpoint(hass, aioclient_mock): payload = aioclient_mock.mock_calls[0] assert str(payload[1]) == ANALYTICS_ENDPOINT_URL + + +async def test_send_with_no_energy(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = False + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert "energy" not in postdata + + +async def test_send_with_no_energy_config(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = False + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert not postdata["energy"]["configured"] + + +async def test_send_with_energy_config(hass, aioclient_mock): + """Test send base prefrences are defined.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert await async_setup_component( + hass, "energy", {"recorder": {"db_url": "sqlite://"}} + ) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ), patch( + "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() + ) as energy_is_configured: + energy_is_configured.return_value = True + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + postdata = aioclient_mock.mock_calls[-1][2] + + assert postdata["energy"]["configured"] diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 80ede3a7548..92e6cf3a5b5 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,7 +1,7 @@ """Test the Energy websocket API.""" import pytest -from homeassistant.components.energy import data +from homeassistant.components.energy import data, is_configured from homeassistant.setup import async_setup_component from tests.common import flush_store @@ -34,6 +34,8 @@ async def test_get_preferences_default(hass, hass_ws_client, hass_storage) -> No manager.data = data.EnergyManager.default_preferences() client = await hass_ws_client(hass) + assert not await is_configured(hass) + await client.send_json({"id": 5, "type": "energy/get_prefs"}) msg = await client.receive_json() @@ -119,6 +121,8 @@ async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: assert hass_storage[data.STORAGE_KEY]["data"] == new_prefs + assert await is_configured(hass) + # Verify info reflects data. await client.send_json({"id": 7, "type": "energy/info"}) From bebd495e740f8b38bd04ea4ebc085b650d6c5117 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 2 Aug 2021 14:55:52 +0200 Subject: [PATCH 081/199] Allow combinations write_coil/read_coils and write_coils/read_coil for modbus switch (#53856) --- homeassistant/components/modbus/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8936ffc32ac..16be39230db 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -198,6 +198,8 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_COIL, + CALL_TYPE_X_COILS, + CALL_TYPE_X_REGISTER_HOLDINGS, ] ), vol.Optional(CONF_STATE_OFF): cv.positive_int, From e35b5dd7c1a07ced9303a0b8ef7e9116aeeedc5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 15:00:43 +0200 Subject: [PATCH 082/199] Add RPi.GPIO dependency to rpi_rf integration (#53858) --- homeassistant/components/rpi_rf/manifest.json | 2 +- requirements_all.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index e8806710724..022c84eb13f 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -2,7 +2,7 @@ "domain": "rpi_rf", "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", - "requirements": ["rpi-rf==0.9.7"], + "requirements": ["rpi-rf==0.9.7", "RPi.GPIO==0.7.1a4"], "codeowners": [], "iot_class": "assumed_state" } diff --git a/requirements_all.txt b/requirements_all.txt index 7453a3cf2c1..204ef9c75a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,6 +69,7 @@ PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio +# homeassistant.components.rpi_rf # RPi.GPIO==0.7.1a4 # homeassistant.components.remember_the_milk From 249fb51d2fc7bce4207ec2299b3de6cb1e43d553 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 16:33:13 +0200 Subject: [PATCH 083/199] Fix cloud accountlinking replacing token data (#53865) --- homeassistant/components/cloud/account_link.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index df93ca6a6ab..5ad7ddcffed 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -143,6 +143,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def _async_refresh_token(self, token: dict) -> dict: """Refresh a token.""" - return await account_link.async_fetch_access_token( + new_token = await account_link.async_fetch_access_token( self.hass.data[DOMAIN], self.service, token["refresh_token"] ) + return {**token, **new_token} From df20d69fd2aeaadd779a0f5dd20837eddc35792a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 16:33:27 +0200 Subject: [PATCH 084/199] Add measurement state class to ZHA power devices (#53866) --- homeassistant/components/zha/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b9a86b79063..3c3aba919ed 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -214,6 +214,7 @@ class ElectricalMeasurement(Sensor): SENSOR_ATTR = "active_power" _device_class = DEVICE_CLASS_POWER + _state_class = STATE_CLASS_MEASUREMENT _unit = POWER_WATT @property From b0f6e8c40a36610db18a50975450dfe9d3ab716b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Aug 2021 18:48:17 +0200 Subject: [PATCH 085/199] Fix growat server config entry missing URL key (#53867) --- homeassistant/components/growatt_server/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 9d0aa098051..fe6bdeb70e8 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -598,7 +598,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = config_entry.data username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - url = config[CONF_URL] + url = config.get(CONF_URL, DEFAULT_URL) name = config[CONF_NAME] api = growattServer.GrowattApi() From 66711219c704286b9778a540fedfe734fdcd4683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 2 Aug 2021 19:35:11 +0200 Subject: [PATCH 086/199] Fix issue when data is None (#53875) --- homeassistant/components/energy/__init__.py | 2 ++ tests/components/energy/test_websocket_api.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py index c856ffb1541..30a1bf8e877 100644 --- a/homeassistant/components/energy/__init__.py +++ b/homeassistant/components/energy/__init__.py @@ -14,6 +14,8 @@ from .data import async_get_manager async def is_configured(hass: HomeAssistant) -> bool: """Return a boolean to indicate if energy is configured.""" manager = await async_get_manager(hass) + if manager.data is None: + return False return bool(manager.data != manager.default_preferences()) diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 92e6cf3a5b5..a14a8d0986e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -30,6 +30,7 @@ async def test_get_preferences_no_data(hass, hass_ws_client) -> None: async def test_get_preferences_default(hass, hass_ws_client, hass_storage) -> None: """Test we get preferences.""" + assert not await is_configured(hass) manager = await data.async_get_manager(hass) manager.data = data.EnergyManager.default_preferences() client = await hass_ws_client(hass) From a97e480d82ca5f7f7a37fb038d8f2f5edbd12dd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 10:42:04 -0700 Subject: [PATCH 087/199] Bump frontend to 20210802.0 (#53876) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 84de9b92c97..f1dfa75864d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210801.0" + "home-assistant-frontend==20210802.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index db6ffd1d071..983831baaa0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 204ef9c75a5..09fb781cf65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04c093ad9a3..347fb0681e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210801.0 +home-assistant-frontend==20210802.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 92cc51370d650531204175ddb8a1c4e004c50bbf Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 2 Aug 2021 18:47:11 +0100 Subject: [PATCH 088/199] Fix watts unit for homekit_controller power sensors (#53877) --- homeassistant/components/homekit_controller/sensor.py | 7 ++++--- .../specific_devices/test_koogeek_p1eu.py | 2 ++ .../specific_devices/test_koogeek_sw2.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 6bf8a7fc084..91b62b0d572 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -29,19 +30,19 @@ SIMPLE_SENSOR = { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, "state_class": STATE_CLASS_MEASUREMENT, - "unit": "watts", + "unit": POWER_WATT, }, CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { "name": "Current Temperature", diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index 6302013223d..1761edb3c8c 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -1,5 +1,6 @@ """Make sure that existing Koogeek P1EU support isn't broken.""" +from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -48,6 +49,7 @@ async def test_koogeek_p1eu_setup(hass): ) state = await helper.poll_and_get_state() assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0 - Real Time Energy" + assert state.attributes["unit_of_measurement"] == POWER_WATT # The sensor and switch should be part of the same device assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 1d46b633153..768959e0331 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -6,6 +6,7 @@ This Koogeek device has a custom power sensor that extra handling. It should have 2 entities - the actual switch and a sensor for power usage. """ +from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -58,6 +59,7 @@ async def test_koogeek_ls1_setup(hass): # Assert that the friendly name is detected correctly assert state.attributes["friendly_name"] == "Koogeek-SW2-187A91 - Real Time Energy" + assert state.attributes["unit_of_measurement"] == POWER_WATT device_registry = dr.async_get(hass) From 2e1f42937d9af4f82b17128ac74778c3e8ba914d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 10:53:52 -0700 Subject: [PATCH 089/199] Bumped version to 2021.8.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0afc18f43bb..bc465d3d742 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From f7e448c8b23cfe4b5cd3ea1a9b101604f61d9ad5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 2 Aug 2021 22:07:37 +0200 Subject: [PATCH 090/199] ESPHome implement light color modes (#53854) --- homeassistant/components/esphome/light.py | 230 ++++++++++++++---- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 190 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index ba968900ef0..b89a75ab76a 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,35 +1,45 @@ """Support for ESPHome lights.""" from __future__ import annotations -from typing import Any +from typing import Any, cast -from aioesphomeapi import LightInfo, LightState +from aioesphomeapi import APIVersion, LightColorMode, LightInfo, LightState from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + ATTR_WHITE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_WHITE, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.util.color as color_util -from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} @@ -49,6 +59,22 @@ async def async_setup_entry( ) +_COLOR_MODES: EsphomeEnumMapper[LightColorMode, str] = EsphomeEnumMapper( + { + LightColorMode.UNKNOWN: COLOR_MODE_UNKNOWN, + LightColorMode.ON_OFF: COLOR_MODE_ONOFF, + LightColorMode.BRIGHTNESS: COLOR_MODE_BRIGHTNESS, + LightColorMode.WHITE: COLOR_MODE_WHITE, + LightColorMode.COLOR_TEMPERATURE: COLOR_MODE_COLOR_TEMP, + LightColorMode.COLD_WARM_WHITE: COLOR_MODE_COLOR_TEMP, + LightColorMode.RGB: COLOR_MODE_RGB, + LightColorMode.RGB_WHITE: COLOR_MODE_RGBW, + LightColorMode.RGB_COLOR_TEMPERATURE: COLOR_MODE_RGBWW, + LightColorMode.RGB_COLD_WARM_WHITE: COLOR_MODE_RGBWW, + } +) + + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @@ -56,6 +82,11 @@ async def async_setup_entry( class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """A light implementation for ESPHome.""" + @property + def _supports_color_mode(self) -> bool: + """Return whether the client supports the new color mode system natively.""" + return self._api_version >= APIVersion(1, 6) + @esphome_state_property def is_on(self) -> bool | None: # type: ignore[override] """Return true if the light is on.""" @@ -64,22 +95,80 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" data: dict[str, Any] = {"key": self._static_info.key, "state": True} - if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs[ATTR_HS_COLOR] - red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) - data["rgb"] = (red / 255, green / 255, blue / 255) - if ATTR_FLASH in kwargs: - data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] - if ATTR_TRANSITION in kwargs: - data["transition_length"] = kwargs[ATTR_TRANSITION] - if ATTR_BRIGHTNESS in kwargs: - data["brightness"] = kwargs[ATTR_BRIGHTNESS] / 255 - if ATTR_COLOR_TEMP in kwargs: - data["color_temperature"] = kwargs[ATTR_COLOR_TEMP] - if ATTR_EFFECT in kwargs: - data["effect"] = kwargs[ATTR_EFFECT] - if ATTR_WHITE_VALUE in kwargs: - data["white"] = kwargs[ATTR_WHITE_VALUE] / 255 + # rgb/brightness input is in range 0-255, but esphome uses 0-1 + + if (brightness_ha := kwargs.get(ATTR_BRIGHTNESS)) is not None: + data["brightness"] = brightness_ha / 255 + + if (rgb_ha := kwargs.get(ATTR_RGB_COLOR)) is not None: + rgb = tuple(x / 255 for x in rgb_ha) + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = LightColorMode.RGB + + if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: + # pylint: disable=invalid-name + *rgb, w = tuple(x / 255 for x in rgbw_ha) # type: ignore[assignment] + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + data["white"] = w + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = LightColorMode.RGB_WHITE + + if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: + # pylint: disable=invalid-name + *rgb, cw, ww = tuple(x / 255 for x in rgbww_ha) # type: ignore[assignment] + color_bri = max(rgb) + # normalize rgb + data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + modes = self._native_supported_color_modes + if ( + self._supports_color_mode + and LightColorMode.RGB_COLD_WARM_WHITE in modes + ): + data["cold_white"] = cw + data["warm_white"] = ww + target_mode = LightColorMode.RGB_COLD_WARM_WHITE + else: + # need to convert cw+ww part to white+color_temp + white = data["white"] = max(cw, ww) + if white != 0: + min_ct = self.min_mireds + max_ct = self.max_mireds + ct_ratio = ww / (cw + ww) + data["color_temperature"] = min_ct + ct_ratio * (max_ct - min_ct) + target_mode = LightColorMode.RGB_COLOR_TEMPERATURE + + if self._supports_color_mode: + data["color_brightness"] = color_bri + data["color_mode"] = target_mode + + if (flash := kwargs.get(ATTR_FLASH)) is not None: + data["flash_length"] = FLASH_LENGTHS[flash] + + if (transition := kwargs.get(ATTR_TRANSITION)) is not None: + data["transition_length"] = transition + + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + data["color_temperature"] = color_temp + if self._supports_color_mode: + data["color_mode"] = LightColorMode.COLOR_TEMPERATURE + + if (effect := kwargs.get(ATTR_EFFECT)) is not None: + data["effect"] = effect + + if (white_ha := kwargs.get(ATTR_WHITE)) is not None: + # ESPHome multiplies brightness and white together for final brightness + # HA only sends `white` in turn_on, and reads total brightness through brightness property + data["brightness"] = white_ha / 255 + data["white"] = 1.0 + data["color_mode"] = LightColorMode.WHITE + await self._client.light_command(**data) async def async_turn_off(self, **kwargs: Any) -> None: @@ -97,10 +186,65 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return round(self._state.brightness * 255) @esphome_state_property - def hs_color(self) -> tuple[float, float] | None: - """Return the hue and saturation color value [float, float].""" - return color_util.color_RGB_to_hs( - self._state.red * 255, self._state.green * 255, self._state.blue * 255 + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if not self._supports_color_mode: + supported = self.supported_color_modes + if not supported: + return None + return next(iter(supported)) + + return _COLOR_MODES.from_esphome(self._state.color_mode) + + @esphome_state_property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + if not self._supports_color_mode: + return ( + round(self._state.red * 255), + round(self._state.green * 255), + round(self._state.blue * 255), + ) + + return ( + round(self._state.red * self._state.color_brightness * 255), + round(self._state.green * self._state.color_brightness * 255), + round(self._state.blue * self._state.color_brightness * 255), + ) + + @esphome_state_property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + white = round(self._state.white * 255) + rgb = cast("tuple[int, int, int]", self.rgb_color) + return (*rgb, white) + + @esphome_state_property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + rgb = cast("tuple[int, int, int]", self.rgb_color) + if ( + not self._supports_color_mode + or self._state.color_mode != LightColorMode.RGB_COLD_WARM_WHITE + ): + # Try to reverse white + color temp to cwww + min_ct = self._static_info.min_mireds + max_ct = self._static_info.max_mireds + color_temp = self._state.color_temperature + white = self._state.white + + ww_frac = (color_temp - min_ct) / (max_ct - min_ct) + cw_frac = 1 - ww_frac + + return ( + *rgb, + round(white * cw_frac / max(cw_frac, ww_frac) * 255), + round(white * ww_frac / max(cw_frac, ww_frac) * 255), + ) + return ( + *rgb, + round(self._state.cold_white * 255), + round(self._state.warm_white * 255), ) @esphome_state_property @@ -108,33 +252,33 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return the CT color value in mireds.""" return self._state.color_temperature - @esphome_state_property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - return round(self._state.white * 255) - @esphome_state_property def effect(self) -> str | None: """Return the current effect.""" return self._state.effect + @property + def _native_supported_color_modes(self) -> list[LightColorMode]: + return self._static_info.supported_color_modes_compat(self._api_version) + @property def supported_features(self) -> int: """Flag supported features.""" flags = SUPPORT_FLASH - if self._static_info.supports_brightness: - flags |= SUPPORT_BRIGHTNESS + + # All color modes except UNKNOWN,ON_OFF support transition + modes = self._native_supported_color_modes + if any(m not in (LightColorMode.UNKNOWN, LightColorMode.ON_OFF) for m in modes): flags |= SUPPORT_TRANSITION - if self._static_info.supports_rgb: - flags |= SUPPORT_COLOR - if self._static_info.supports_white_value: - flags |= SUPPORT_WHITE_VALUE - if self._static_info.supports_color_temperature: - flags |= SUPPORT_COLOR_TEMP if self._static_info.effects: flags |= SUPPORT_EFFECT return flags + @property + def supported_color_modes(self) -> set[str] | None: + """Flag supported color modes.""" + return set(map(_COLOR_MODES.from_esphome, self._native_supported_color_modes)) + @property def effect_list(self) -> list[str]: """Return the list of supported effects.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 218c349e5a8..a6792808720 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==5.1.1"], + "requirements": ["aioesphomeapi==6.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 09fb781cf65..3622b435c8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.1.1 +aioesphomeapi==6.0.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 347fb0681e1..79abf95dd58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==5.1.1 +aioesphomeapi==6.0.0 # homeassistant.components.flo aioflo==0.4.1 From be6cb2e79240c3461f765c29becf518c70f16ba9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 14:44:15 -0700 Subject: [PATCH 091/199] Bump aiohue to 2.6.1 (#53887) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 05e69948218..32b3cd4ee51 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.6.0"], + "requirements": ["aiohue==2.6.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3622b435c8e..84e076fabec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.0 +aiohue==2.6.1 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79abf95dd58..75f6c0a1239 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -122,7 +122,7 @@ aiohomekit==0.6.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.0 +aiohue==2.6.1 # homeassistant.components.apache_kafka aiokafka==0.6.0 From d74ca2529126febc49a15dc4c036874d776c8a1c Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 2 Aug 2021 20:50:57 -0700 Subject: [PATCH 092/199] Handle powerConsumption reports with null value (#53888) --- .../components/smartthings/sensor.py | 11 +++++++++- tests/components/smartthings/test_sensor.py | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 5059bcc4403..cb8fa4bb6d2 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -554,6 +554,8 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): """Init the class.""" super().__init__(device) self.report_name = report_name + # This is an exception for STATE_CLASS_MEASUREMENT per @balloob + self._attr_state_class = STATE_CLASS_MEASUREMENT @property def name(self) -> str: @@ -569,7 +571,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): def state(self): """Return the state of the sensor.""" value = self._device.status.attributes[Attribute.power_consumption].value - if value.get(self.report_name) is None: + if value is None or value.get(self.report_name) is None: return None if self.report_name == "power": return value[self.report_name] @@ -588,3 +590,10 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): if self.report_name == "power": return POWER_WATT return ENERGY_KILO_WATT_HOUR + + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + if self.report_name != "power": + return utc_from_timestamp(0) + return None diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index fa849a3cc67..70103c3a837 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -187,6 +187,28 @@ async def test_power_consumption_sensor(hass, device_factory): assert entry.model == device.device_type_name assert entry.manufacturer == "Unavailable" + device = device_factory( + "vacuum", + [Capability.power_consumption_report], + {Attribute.power_consumption: {}}, + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + # Act + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + # Assert + state = hass.states.get("sensor.vacuum_energy") + assert state + assert state.state == "unknown" + entry = entity_registry.async_get("sensor.vacuum_energy") + assert entry + assert entry.unique_id == f"{device.device_id}.energy" + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == "Unavailable" + async def test_update_from_signal(hass, device_factory): """Test the binary_sensor updates when receiving a signal.""" From 8b7bdc9b67cb54c92b96d889969aec734f211671 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 2 Aug 2021 21:52:44 -0600 Subject: [PATCH 093/199] Only show a SimpliSafe code entry when one exists (#53894) --- .../components/simplisafe/alarm_control_panel.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 5c50d6a343e..7e0b64a8c32 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,8 +1,6 @@ """Support for SimpliSafe alarm control panels.""" from __future__ import annotations -import re - from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v2 import SystemV2 @@ -72,12 +70,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - if isinstance( - self._simplisafe.config_entry.options.get(CONF_CODE), str - ) and re.search("^\\d+$", self._simplisafe.config_entry.options[CONF_CODE]): - self._attr_code_format = FORMAT_NUMBER - else: - self._attr_code_format = FORMAT_TEXT + if CONF_CODE in self._simplisafe.config_entry.options: + if self._simplisafe.config_entry.options[CONF_CODE].isdigit(): + self._attr_code_format = FORMAT_NUMBER + else: + self._attr_code_format = FORMAT_TEXT self._attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY self._last_event = None From 120122ffe2bfec5ceaff6ba0c3e8359417b5732b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 21:39:53 -0700 Subject: [PATCH 094/199] Bump frontend to 20210803.0 (#53897) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f1dfa75864d..a94bcf44327 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210802.0" + "home-assistant-frontend==20210803.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 983831baaa0..9dfcc96d6da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 84e076fabec..edf77033645 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75f6c0a1239..754097e8317 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210802.0 +home-assistant-frontend==20210803.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5536da18a6a57ddba9a3d9bdc7111da8d97eaf74 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Aug 2021 21:41:32 -0700 Subject: [PATCH 095/199] Bumped version to 2021.8.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bc465d3d742..d5b48312f3c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 30497eff0ed0d0070c718ce4988906f1edcbe8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 3 Aug 2021 11:58:27 +0200 Subject: [PATCH 096/199] Add user to homeassistant system health (#53902) --- .../components/homeassistant/strings.json | 3 ++- .../components/homeassistant/system_health.py | 1 + homeassistant/helpers/system_info.py | 5 ++++- tests/helpers/test_system_info.py | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 7da4a5a9d8a..09be9283b5c 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -4,6 +4,7 @@ "arch": "CPU Architecture", "dev": "Development", "docker": "Docker", + "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", @@ -14,4 +15,4 @@ "virtualenv": "Virtual Environment" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index ff3562a24f9..f13278ddfeb 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -22,6 +22,7 @@ async def system_health_info(hass): "dev": info.get("dev"), "hassio": info.get("hassio"), "docker": info.get("docker"), + "user": info.get("user"), "virtualenv": info.get("virtualenv"), "python_version": info.get("python_version"), "os_name": info.get("os_name"), diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 6d6c912f8c9..766fa90af96 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -1,6 +1,7 @@ """Helper to gather system info.""" from __future__ import annotations +from getpass import getuser import os import platform from typing import Any @@ -22,6 +23,7 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: "virtualenv": is_virtual_env(), "python_version": platform.python_version(), "docker": False, + "user": getuser(), "arch": platform.machine(), "timezone": str(hass.config.time_zone), "os_name": platform.system(), @@ -37,7 +39,8 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: # Determine installation type on current data if info_object["docker"]: - info_object["installation_type"] = "Home Assistant Container" + if info_object["user"] == "root": + info_object["installation_type"] = "Home Assistant Container" elif is_virtual_env(): info_object["installation_type"] = "Home Assistant Core" diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index e27114c1a13..fd9d488596f 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -1,5 +1,6 @@ """Tests for the system info helper.""" import json +from unittest.mock import patch from homeassistant.const import __version__ as current_version @@ -9,4 +10,20 @@ async def test_get_system_info(hass): info = await hass.helpers.system_info.async_get_system_info() assert isinstance(info, dict) assert info["version"] == current_version + assert info["user"] is not None assert json.dumps(info) is not None + + +async def test_container_installationtype(hass): + """Test container installation type.""" + with patch("platform.system", return_value="Linux"), patch( + "os.path.isfile", return_value=True + ): + info = await hass.helpers.system_info.async_get_system_info() + assert info["installation_type"] == "Home Assistant Container" + + with patch("platform.system", return_value="Linux"), patch( + "os.path.isfile", return_value=True + ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): + info = await hass.helpers.system_info.async_get_system_info() + assert info["installation_type"] == "Unknown" From 922c0dc8bee7ce2be94052c7497949eecc580e6f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Aug 2021 23:39:35 +1200 Subject: [PATCH 097/199] Bump aioesphomeapi to 6.0.1 (#53905) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a6792808720..22fa33091fd 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==6.0.0"], + "requirements": ["aioesphomeapi==6.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index edf77033645..402ca09358c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==6.0.0 +aioesphomeapi==6.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754097e8317..96d8f9660d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==6.0.0 +aioesphomeapi==6.0.1 # homeassistant.components.flo aioflo==0.4.1 From 07604e60e5ba6967bdc5e875bdc98897f30e8ddb Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Tue, 3 Aug 2021 12:52:59 +0100 Subject: [PATCH 098/199] Bump pyroon to 0.0.38 (#53906) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 354117e8fe4..f4864571735 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.37"], + "requirements": ["roonapi==0.0.38"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 402ca09358c..46623a817a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.37 +roonapi==0.0.38 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96d8f9660d0..8099effd980 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1125,7 +1125,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.37 +roonapi==0.0.38 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From e4fd43ed7ca8c3a114844e5ddcdc47103d6d2b2b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 3 Aug 2021 15:58:30 +0200 Subject: [PATCH 099/199] Use `SelectEntityDescription` for Xiaomi Miio integration (#53907) * Use SelectEntityDescription * Use SelectEntityDescription * Remove service field from XiaomiMiioSelectDescription class * Fix typo * Use lowercase for options --- .../components/xiaomi_miio/select.py | 67 +++++++++---------- .../xiaomi_miio/strings.select.json | 9 +++ 2 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/strings.select.json diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 77aba961244..c5cee6221fa 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,11 +1,13 @@ """Support led_brightness for Mi Air Humidifier.""" +from __future__ import annotations + from dataclasses import dataclass from enum import Enum from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness -from homeassistant.components.select import SelectEntity +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import callback from .const import ( @@ -20,7 +22,6 @@ from .const import ( MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, MODELS_HUMIDIFIER, - SERVICE_SET_LED_BRIGHTNESS, ) from .device import XiaomiCoordinatedMiioEntity @@ -36,23 +37,19 @@ LED_BRIGHTNESS_REVERSE_MAP_MIOT = { @dataclass -class SelectorType: - """Class that holds device specific info for a xiaomi aqara or humidifier selectors.""" +class XiaomiMiioSelectDescription(SelectEntityDescription): + """A class that describes select entities.""" - name: str = None - icon: str = None - short_name: str = None - options: list = None - service: str = None + options: tuple = () SELECTOR_TYPES = { - FEATURE_SET_LED_BRIGHTNESS: SelectorType( - name="Led brightness", + FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioSelectDescription( + key=ATTR_LED_BRIGHTNESS, + name="Led Brightness", icon="mdi:brightness-6", - short_name=ATTR_LED_BRIGHTNESS, - options=["Bright", "Dim", "Off"], - service=SERVICE_SET_LED_BRIGHTNESS, + device_class="xiaomi_miio__led_brightness", + options=("bright", "dim", "off"), ), } @@ -65,7 +62,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: entity_class = XiaomiAirHumidifierSelector @@ -76,17 +72,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else: return - for selector in SELECTOR_TYPES.values(): - entities.append( - entity_class( - f"{config_entry.title} {selector.name}", - device, - config_entry, - f"{selector.short_name}_{config_entry.unique_id}", - selector, - coordinator, - ) + description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] + entities.append( + entity_class( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{description.key}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, ) + ) async_add_entities(entities) @@ -94,12 +90,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, selector, coordinator): + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" super().__init__(name, device, entry, unique_id, coordinator) - self._attr_icon = selector.icon - self._controller = selector - self._attr_options = self._controller.options + self._attr_options = list(description.options) + self.entity_description = description @staticmethod def _extract_value_from_attribute(state, attribute): @@ -113,33 +108,33 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" - def __init__(self, name, device, entry, unique_id, controller, coordinator): + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, controller, coordinator) + super().__init__(name, device, entry, unique_id, coordinator, description) self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self._controller.short_name + self.coordinator.data, self.entity_description.key ) @callback def _handle_coordinator_update(self): """Fetch state from the device.""" self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self._controller.short_name + self.coordinator.data, self.entity_description.key ) self.async_write_ha_state() @property def current_option(self): """Return the current option.""" - return self.led_brightness + return self.led_brightness.lower() async def async_select_option(self, option: str) -> None: """Set an option of the miio device.""" if option not in self.options: raise ValueError( - f"Selection '{option}' is not a valid {self._controller.name}" + f"Selection '{option}' is not a valid {self.entity_description.name}" ) - await self.async_set_led_brightness(option) + await self.async_set_led_brightness(option.title()) @property def led_brightness(self): diff --git a/homeassistant/components/xiaomi_miio/strings.select.json b/homeassistant/components/xiaomi_miio/strings.select.json new file mode 100644 index 00000000000..80edde042ce --- /dev/null +++ b/homeassistant/components/xiaomi_miio/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Bright", + "dim": "Dim", + "off": "Off" + } + } + } \ No newline at end of file From 4e2c1747413f03e2302dce878714856ffb3bbd31 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Aug 2021 08:56:15 -0600 Subject: [PATCH 100/199] Handle scenario where SimpliSafe code is falsey (#53912) --- homeassistant/components/simplisafe/alarm_control_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 7e0b64a8c32..8520cd2b50f 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -70,8 +70,8 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - if CONF_CODE in self._simplisafe.config_entry.options: - if self._simplisafe.config_entry.options[CONF_CODE].isdigit(): + if code := self._simplisafe.config_entry.options.get(CONF_CODE): + if code.isdigit(): self._attr_code_format = FORMAT_NUMBER else: self._attr_code_format = FORMAT_TEXT From 54ac889362a92a4442054db41a22d5d13f14fe5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Aug 2021 12:09:10 -0500 Subject: [PATCH 101/199] Enforce maximum length for HomeKit characteristics (#53913) --- .../components/homekit/accessories.py | 17 ++++++--- homeassistant/components/homekit/const.py | 7 ++++ homeassistant/components/homekit/type_fans.py | 4 +- .../components/homekit/type_media_players.py | 3 +- .../components/homekit/type_remotes.py | 7 +++- tests/components/homekit/test_accessories.py | 37 +++++++++++++------ 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 03d00c42a91..49ba1103ac5 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -64,6 +64,11 @@ from .const import ( HK_NOT_CHARGABLE, HK_NOT_CHARGING, MANUFACTURER, + MAX_MANUFACTURER_LENGTH, + MAX_MODEL_LENGTH, + MAX_NAME_LENGTH, + MAX_SERIAL_LENGTH, + MAX_VERSION_LENGTH, SERV_BATTERY_SERVICE, SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, @@ -217,7 +222,9 @@ class HomeAccessory(Accessory): **kwargs, ): """Initialize a Accessory object.""" - super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs) + super().__init__( + driver=driver, display_name=name[:MAX_NAME_LENGTH], aid=aid, *args, **kwargs + ) self.config = config or {} domain = split_entity_id(entity_id)[0].replace("_", " ") @@ -237,10 +244,10 @@ class HomeAccessory(Accessory): sw_version = __version__ self.set_info_service( - manufacturer=manufacturer, - model=model, - serial_number=entity_id, - firmware_revision=sw_version, + manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH], + model=model[:MAX_MODEL_LENGTH], + serial_number=entity_id[:MAX_SERIAL_LENGTH], + firmware_revision=sw_version[:MAX_VERSION_LENGTH], ) self.category = category diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 4fecd64b2b2..7f413ef78df 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -296,3 +296,10 @@ CONFIG_OPTIONS = [ CONF_ENTITY_CONFIG, CONF_HOMEKIT_MODE, ] + +# ### Maximum Lengths ### +MAX_NAME_LENGTH = 64 +MAX_SERIAL_LENGTH = 64 +MAX_MODEL_LENGTH = 64 +MAX_VERSION_LENGTH = 64 +MAX_MANUFACTURER_LENGTH = 64 diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 1efb3b6c8be..1a0bb41774c 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -39,6 +39,7 @@ from .const import ( CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + MAX_NAME_LENGTH, PROP_MIN_STEP, SERV_FANV2, SERV_SWITCH, @@ -100,7 +101,8 @@ class Fan(HomeAccessory): preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_fan.add_linked_service(preset_serv) preset_serv.configure_char( - CHAR_NAME, value=f"{self.display_name} {preset_mode}" + CHAR_NAME, + value=f"{self.display_name} {preset_mode}"[:MAX_NAME_LENGTH], ) self.preset_mode_chars[preset_mode] = preset_serv.configure_char( diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 5cd27109bd8..081053d2591 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -55,6 +55,7 @@ from .const import ( FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, KEY_PLAY_PAUSE, + MAX_NAME_LENGTH, SERV_SWITCH, SERV_TELEVISION_SPEAKER, ) @@ -134,7 +135,7 @@ class MediaPlayer(HomeAccessory): def generate_service_name(self, mode): """Generate name for individual service.""" - return f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}" + return f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}"[:MAX_NAME_LENGTH] def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 718671dfd1d..9e54221430c 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -47,6 +47,7 @@ from .const import ( KEY_PREVIOUS_TRACK, KEY_REWIND, KEY_SELECT, + MAX_NAME_LENGTH, SERV_INPUT_SOURCE, SERV_TELEVISION, ) @@ -120,8 +121,10 @@ class RemoteInputSelectAccessory(HomeAccessory): SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] ) serv_tv.add_linked_service(serv_input) - serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) - serv_input.configure_char(CHAR_NAME, value=source) + serv_input.configure_char( + CHAR_CONFIGURED_NAME, value=source[:MAX_NAME_LENGTH] + ) + serv_input.configure_char(CHAR_NAME, value=source[:MAX_NAME_LENGTH]) serv_input.configure_char(CHAR_IDENTIFIER, value=index) serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) input_type = 3 if "hdmi" in source.lower() else 0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 84ed61322a2..257be293fc0 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -66,7 +66,7 @@ async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = "sensor.accessory" - entity_id2 = "light.accessory" + entity_id2 = "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum_maximum_maximum_maximum_allowed_length" hass.states.async_set(entity_id, None) hass.states.async_set(entity_id2, STATE_UNAVAILABLE) @@ -94,27 +94,42 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" assert serv.get_characteristic(CHAR_MANUFACTURER).value == f"{MANUFACTURER} Light" assert serv.get_characteristic(CHAR_MODEL).value == "Light" - assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) acc3 = HomeAccessory( hass, hk_driver, - "Home Accessory", + "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length", entity_id2, 3, { - ATTR_MODEL: "Awesome", - ATTR_MANUFACTURER: "Lux Brands", - ATTR_SOFTWARE_VERSION: "0.4.3", - ATTR_INTEGRATION: "luxe", + ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_SOFTWARE_VERSION: "0.4.3 that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_INTEGRATION: "luxe that exceeds the maximum maximum maximum maximum maximum maximum length", }, ) assert acc3.available is False serv = acc3.services[0] # SERV_ACCESSORY_INFO - assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" - assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands" - assert serv.get_characteristic(CHAR_MODEL).value == "Awesome" - assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" + assert ( + serv.get_characteristic(CHAR_NAME).value + == "Home Accessory that exceeds the maximum maximum maximum maximum " + ) + assert ( + serv.get_characteristic(CHAR_MANUFACTURER).value + == "Lux Brands that exceeds the maximum maximum maximum maximum maxi" + ) + assert ( + serv.get_characteristic(CHAR_MODEL).value + == "Awesome Model that exceeds the maximum maximum maximum maximum m" + ) + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() From 7a8676dc831577290b2ffd1f183dba2cf8eccfb0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Aug 2021 11:16:00 -0700 Subject: [PATCH 102/199] Handle Shelly get name on uninitialized device (#53917) --- homeassistant/components/shelly/logbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 5b0ada6f166..deac3b5c05b 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -29,7 +29,7 @@ def async_describe_events( def async_describe_shelly_click_event(event: EventType) -> dict[str, str]: """Describe shelly.click logbook event.""" wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) - if wrapper: + if wrapper and wrapper.device.initialized: device_name = get_device_name(wrapper.device) else: device_name = event.data[ATTR_DEVICE] From 0342c0da3392b864731cc0d1bd051502364a9a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 3 Aug 2021 20:20:12 +0200 Subject: [PATCH 103/199] Limit API usage for Uptime Robot (#53918) --- .../components/uptimerobot/binary_sensor.py | 137 +++++++++++------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 6c0bb63c70f..dd7254fb1ca 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,6 +1,9 @@ """A platform that to monitor Uptime Robot monitors.""" +from dataclasses import dataclass +from datetime import timedelta import logging +import async_timeout from pyuptimerobot import UptimeRobot import voluptuous as vol @@ -8,9 +11,17 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, PLATFORM_SCHEMA, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -21,69 +32,91 @@ ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def setup_platform(hass, config, add_entities, discovery_info=None): +@dataclass +class UptimeRobotBinarySensorEntityDescription(BinarySensorEntityDescription): + """Entity description for UptimeRobotBinarySensor.""" + + target: str = "" + + +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None +): """Set up the Uptime Robot binary_sensors.""" + uptime_robot_api = UptimeRobot() + api_key = config[CONF_API_KEY] - up_robot = UptimeRobot() - api_key = config.get(CONF_API_KEY) - monitors = up_robot.getMonitors(api_key) + async def async_update_data(): + """Fetch data from API UptimeRobot API.""" - devices = [] - if not monitors or monitors.get("stat") != "ok": + def api_wrapper(): + return uptime_robot_api.getMonitors(api_key) + + async with async_timeout.timeout(10): + monitors = await hass.async_add_executor_job(api_wrapper) + if not monitors or monitors.get("stat") != "ok": + raise UpdateFailed("Error communicating with Uptime Robot API") + return monitors + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="uptimerobot", + update_method=async_update_data, + update_interval=timedelta(seconds=60), + ) + + await coordinator.async_refresh() + + if not coordinator.data or coordinator.data.get("stat") != "ok": _LOGGER.error("Error connecting to Uptime Robot") - return + raise PlatformNotReady() - for monitor in monitors["monitors"]: - devices.append( + async_add_entities( + [ UptimeRobotBinarySensor( - api_key, - up_robot, - monitor["id"], - monitor["friendly_name"], - monitor["url"], + coordinator, + UptimeRobotBinarySensorEntityDescription( + key=monitor["id"], + name=monitor["friendly_name"], + target=monitor["url"], + device_class=DEVICE_CLASS_CONNECTIVITY, + ), ) - ) - - add_entities(devices, True) + for monitor in coordinator.data["monitors"] + ], + True, + ) -class UptimeRobotBinarySensor(BinarySensorEntity): +class UptimeRobotBinarySensor(BinarySensorEntity, CoordinatorEntity): """Representation of a Uptime Robot binary sensor.""" - def __init__(self, api_key, up_robot, monitor_id, name, target): + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: UptimeRobotBinarySensorEntityDescription, + ) -> None: """Initialize Uptime Robot the binary sensor.""" - self._api_key = api_key - self._monitor_id = str(monitor_id) - self._name = name - self._target = target - self._up_robot = up_robot - self._state = None + super().__init__(coordinator) + self.coordinator = coordinator + self.entity_description = description + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_TARGET: self.entity_description.target, + } - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY - - @property - def extra_state_attributes(self): - """Return the state attributes of the binary sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target} - - def update(self): + async def async_update(self): """Get the latest state of the binary sensor.""" - monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id) - if not monitor or monitor.get("stat") != "ok": - _LOGGER.warning("Failed to get new state") - return - status = monitor["monitors"][0]["status"] - self._state = 1 if status == 2 else 0 + if monitor := get_monitor_by_id( + self.coordinator.data.get("monitors", []), self.entity_description.key + ): + self._attr_is_on = monitor["status"] == 2 + + +def get_monitor_by_id(monitors, monitor_id): + """Return the monitor object matching the id.""" + filtered = [monitor for monitor in monitors if monitor["id"] == monitor_id] + if len(filtered) == 0: + return + return filtered[0] From 4fdd354745ace6145d7f03775e96a1801ad28fb6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Aug 2021 22:50:14 +0200 Subject: [PATCH 104/199] Limit zwave_js meter sensor last reset (#53921) --- homeassistant/components/zwave_js/sensor.py | 20 ++++++++------ tests/components/zwave_js/common.py | 3 ++- tests/components/zwave_js/test_sensor.py | 29 ++++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 304a80f7940..7b491661e68 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -248,20 +248,20 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): # Entity class attributes self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_last_reset = dt.utc_from_timestamp(0) + if self.device_class == DEVICE_CLASS_ENERGY: + self._attr_last_reset = dt.utc_from_timestamp(0) @callback def async_update_last_reset( self, node: ZwaveNode, endpoint: int, meter_type: int | None ) -> None: """Update last reset.""" - # If the signal is not for this node or is for a different endpoint, ignore it - if self.info.node != node or self.info.primary_value.endpoint != endpoint: - return - # If a meter type was specified and doesn't match this entity's meter type, - # ignore it + # If the signal is not for this node or is for a different endpoint, + # or a meter type was specified and doesn't match this entity's meter type: if ( - meter_type is not None + self.info.node != node + or self.info.primary_value.endpoint != endpoint + or meter_type is not None and self.info.primary_value.metadata.cc_specific.get("meterType") != meter_type ): @@ -274,6 +274,10 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): """Call when entity is added.""" await super().async_added_to_hass() + # If the meter is not an accumulating meter type, do not reset. + if self.device_class != DEVICE_CLASS_ENERGY: + return + # Restore the last reset time from stored state restored_state = await self.async_get_last_state() if restored_state and ATTR_LAST_RESET in restored_state.attributes: @@ -310,7 +314,7 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): primary_value.endpoint, options, ) - self._attr_last_reset = dt.utcnow() + # Notify meters that may have been reset async_dispatcher_send( self.hass, diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 8c8a3f2e576..44943fed9fb 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -33,7 +33,8 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" ) ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" -METER_SENSOR = "sensor.smart_switch_6_electric_consumed_v" +METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh" +METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v" DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc) DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 9fa4152ad6b..04583559421 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -36,7 +36,8 @@ from .common import ( HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, - METER_SENSOR, + METER_ENERGY_SENSOR, + METER_VOLTAGE_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, VOLTAGE_SENSOR, @@ -202,9 +203,13 @@ async def test_reset_meter( client.async_send_command.return_value = {} client.async_send_command_no_wait.return_value = {} + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + # Validate that the sensor last reset is starting from nothing assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_ZERO.isoformat() ) @@ -215,13 +220,13 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: METER_SENSOR, + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, }, blocking=True, ) assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_LAST_RESET.isoformat() ) @@ -232,6 +237,10 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [] + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + client.async_send_command_no_wait.reset_mock() # Test successful meter reset call with options @@ -239,7 +248,7 @@ async def test_reset_meter( DOMAIN, SERVICE_RESET_METER, { - ATTR_ENTITY_ID: METER_SENSOR, + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, ATTR_METER_TYPE: 1, ATTR_VALUE: 2, }, @@ -253,6 +262,10 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [{"type": 1, "targetValue": 2}] + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes + client.async_send_command_no_wait.reset_mock() @@ -265,6 +278,10 @@ async def test_restore_last_reset( ): """Test restoring last_reset on setup.""" assert ( - hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET] + hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] == DATETIME_LAST_RESET.isoformat() ) + + # Validate that non accumulating meter does not have a last reset attribute + + assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes From af81dda1e2fa308afc4c6384a23293652d7ce8bb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Aug 2021 02:10:33 +0200 Subject: [PATCH 105/199] Update frontend to 20210803.2 (#53923) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index a94bcf44327..fbf676687cb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210803.0" + "home-assistant-frontend==20210803.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9dfcc96d6da..7b9fe917279 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 46623a817a9..f15f5cf66ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8099effd980..29a8352f433 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.0 +home-assistant-frontend==20210803.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 38df475936af93c7036179b9a038f1087f6ec9a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Aug 2021 21:04:22 -0700 Subject: [PATCH 106/199] Bumped version to 2021.8.0b10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d5b48312f3c..706942e7faa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 9ec516e1d4577eae43ab5cf811b808dc54338d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 Aug 2021 09:29:51 +0200 Subject: [PATCH 107/199] Address review comments for 53918 (#53927) --- .../components/uptimerobot/binary_sensor.py | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index dd7254fb1ca..e1684d64924 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,5 +1,4 @@ """A platform that to monitor Uptime Robot monitors.""" -from dataclasses import dataclass from datetime import timedelta import logging @@ -32,13 +31,6 @@ ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -@dataclass -class UptimeRobotBinarySensorEntityDescription(BinarySensorEntityDescription): - """Entity description for UptimeRobotBinarySensor.""" - - target: str = "" - - async def async_setup_platform( hass: HomeAssistant, config, async_add_entities, discovery_info=None ): @@ -46,12 +38,11 @@ async def async_setup_platform( uptime_robot_api = UptimeRobot() api_key = config[CONF_API_KEY] + def api_wrapper(): + return uptime_robot_api.getMonitors(api_key) + async def async_update_data(): """Fetch data from API UptimeRobot API.""" - - def api_wrapper(): - return uptime_robot_api.getMonitors(api_key) - async with async_timeout.timeout(10): monitors = await hass.async_add_executor_job(api_wrapper) if not monitors or monitors.get("stat") != "ok": @@ -76,16 +67,15 @@ async def async_setup_platform( [ UptimeRobotBinarySensor( coordinator, - UptimeRobotBinarySensorEntityDescription( + BinarySensorEntityDescription( key=monitor["id"], name=monitor["friendly_name"], - target=monitor["url"], device_class=DEVICE_CLASS_CONNECTIVITY, ), + target=monitor["url"], ) for monitor in coordinator.data["monitors"] ], - True, ) @@ -95,28 +85,28 @@ class UptimeRobotBinarySensor(BinarySensorEntity, CoordinatorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - description: UptimeRobotBinarySensorEntityDescription, + description: BinarySensorEntityDescription, + target: str, ) -> None: """Initialize Uptime Robot the binary sensor.""" super().__init__(coordinator) - self.coordinator = coordinator self.entity_description = description + self._target = target self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_TARGET: self.entity_description.target, + ATTR_TARGET: self._target, } - async def async_update(self): - """Get the latest state of the binary sensor.""" - if monitor := get_monitor_by_id( - self.coordinator.data.get("monitors", []), self.entity_description.key + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + if monitor := next( + ( + monitor + for monitor in self.coordinator.data.get("monitors", []) + if monitor["id"] == self.entity_description.key + ), + None, ): - self._attr_is_on = monitor["status"] == 2 - - -def get_monitor_by_id(monitors, monitor_id): - """Return the monitor object matching the id.""" - filtered = [monitor for monitor in monitors if monitor["id"] == monitor_id] - if len(filtered) == 0: - return - return filtered[0] + return monitor["status"] == 2 + return False From 4de3f031cc82e78c0632835b9ce8edcb55877581 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Aug 2021 10:46:20 +0200 Subject: [PATCH 108/199] Bumped version to 2021.8.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 706942e7faa..b95825ae39e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 16b554410438d2edaa3bd9a361a5149d259ca630 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Wed, 4 Aug 2021 22:44:16 +1200 Subject: [PATCH 109/199] Fix Panasonic Viera TV going unavailable when turned off (#53788) --- .../components/panasonic_viera/__init__.py | 8 ++-- tests/components/panasonic_viera/conftest.py | 37 ++++++++++----- .../panasonic_viera/test_media_player.py | 47 +++++++++++++++++++ .../components/panasonic_viera/test_remote.py | 7 ++- 4 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 tests/components/panasonic_viera/test_media_player.py diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index b217be4d4b6..e187b7c18a5 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,7 +1,7 @@ """The Panasonic Viera integration.""" from functools import partial import logging -from urllib.request import URLError +from urllib.request import HTTPError, URLError from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol @@ -247,11 +247,13 @@ class Remote: "The connection couldn't be encrypted. Please reconfigure your TV" ) self.available = False - except (SOAPError): + except (SOAPError, HTTPError) as err: + _LOGGER.debug("An error occurred: %s", err) self.state = STATE_OFF self.available = True await self.async_create_remote_control() - except (URLError, OSError): + except (URLError, OSError) as err: + _LOGGER.debug("An error occurred: %s", err) self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() diff --git a/tests/components/panasonic_viera/conftest.py b/tests/components/panasonic_viera/conftest.py index d1444f01477..e30c0f41e92 100644 --- a/tests/components/panasonic_viera/conftest.py +++ b/tests/components/panasonic_viera/conftest.py @@ -17,8 +17,12 @@ from homeassistant.components.panasonic_viera.const import ( DEFAULT_MODEL_NUMBER, DEFAULT_NAME, DEFAULT_PORT, + DOMAIN, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry MOCK_BASIC_DATA = { CONF_HOST: "0.0.0.0", @@ -74,20 +78,11 @@ def get_mock_remote( mock_remote.authorize_pin_code = authorize_pin_code - def get_device_info(): - return device_info + mock_remote.get_device_info = Mock(return_value=device_info) - mock_remote.get_device_info = get_device_info + mock_remote.send_key = Mock() - def send_key(key): - return - - mock_remote.send_key = Mock(send_key) - - def get_volume(key): - return 100 - - mock_remote.get_volume = Mock(get_volume) + mock_remote.get_volume = Mock(return_value=100) return mock_remote @@ -102,3 +97,21 @@ def mock_remote_fixture(): return_value=mock_remote, ): yield mock_remote + + +@pytest.fixture +async def init_integration(hass: HomeAssistant, mock_remote: Mock) -> MockConfigEntry: + """Set up the Panasonic Viera integration for testing.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_DEVICE_INFO[ATTR_UDN], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA, **MOCK_DEVICE_INFO}, + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/panasonic_viera/test_media_player.py b/tests/components/panasonic_viera/test_media_player.py new file mode 100644 index 00000000000..1203bf1ed51 --- /dev/null +++ b/tests/components/panasonic_viera/test_media_player.py @@ -0,0 +1,47 @@ +"""Test the Panasonic Viera media player entity.""" + +from datetime import timedelta +from unittest.mock import Mock +from urllib.error import HTTPError, URLError + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_media_player_handle_URLerror( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_remote: Mock +) -> None: + """Test remote handle URLError as Unavailable.""" + + state_tv = hass.states.get("media_player.panasonic_viera_tv") + assert state_tv.state == STATE_ON + + # simulate timeout error + mock_remote.get_mute = Mock(side_effect=URLError(None, None)) + + async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + + state_tv = hass.states.get("media_player.panasonic_viera_tv") + assert state_tv.state == STATE_UNAVAILABLE + + +async def test_media_player_handle_HTTPError( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_remote: Mock +) -> None: + """Test remote handle HTTPError as Off.""" + + state_tv = hass.states.get("media_player.panasonic_viera_tv") + assert state_tv.state == STATE_ON + + # simulate http badrequest + mock_remote.get_mute = Mock(side_effect=HTTPError(None, 400, None, None, None)) + + async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + + state_tv = hass.states.get("media_player.panasonic_viera_tv") + assert state_tv.state == STATE_OFF diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index 6bfd7dee8eb..0cf80853351 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -1,8 +1,8 @@ """Test the Panasonic Viera remote entity.""" -from unittest.mock import call +from unittest.mock import Mock, call -from panasonic_viera import Keys +from panasonic_viera import Keys, SOAPError from homeassistant.components.panasonic_viera.const import ATTR_UDN, DOMAIN from homeassistant.components.remote import ( @@ -38,6 +38,9 @@ async def test_onoff(hass, mock_remote): data = {ATTR_ENTITY_ID: "remote.panasonic_viera_tv"} + # simulate tv off when async_update + mock_remote.get_mute = Mock(side_effect=SOAPError) + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_OFF, data) await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) await hass.async_block_till_done() From fcc82d26a472552909ed73e86158f0173a78239a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 5 Aug 2021 02:03:31 +0200 Subject: [PATCH 110/199] Add temporary fix to modbus to solve upstream problem (#53857) --- homeassistant/components/modbus/base_platform.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index b321183fd66..5b3cdfb48ee 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -60,7 +60,11 @@ class BasePlatform(Entity): def __init__(self, hub: ModbusHub, entry: dict[str, Any]) -> None: """Initialize the Modbus binary sensor.""" self._hub = hub - self._slave = entry.get(CONF_SLAVE) + # temporary fix, + # make sure slave is always defined to avoid an error in pymodbus + # attr(in_waiting) not defined. + # see issue #657 and PR #660 in riptideio/pymodbus + self._slave = entry.get(CONF_SLAVE, 0) self._address = int(entry[CONF_ADDRESS]) self._input_type = entry[CONF_INPUT_TYPE] self._value = None From 01299ddd20831d3a7b77038654a9bd9f2204f95d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 4 Aug 2021 21:24:19 +0200 Subject: [PATCH 111/199] Fix attr_unit_of_measurement in update of apcupsd entity (#53947) --- homeassistant/components/apcupsd/sensor.py | 93 +++++++++++----------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index d30625ee793..bf1b8bf6db5 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -26,72 +26,72 @@ _LOGGER = logging.getLogger(__name__) SENSOR_PREFIX = "UPS " SENSOR_TYPES = { - "alarmdel": ["Alarm Delay", "", "mdi:alarm", None], - "ambtemp": ["Ambient Temperature", "", "mdi:thermometer", None], - "apc": ["Status Data", "", "mdi:information-outline", None], - "apcmodel": ["Model", "", "mdi:information-outline", None], - "badbatts": ["Bad Batteries", "", "mdi:information-outline", None], - "battdate": ["Battery Replaced", "", "mdi:calendar-clock", None], - "battstat": ["Battery Status", "", "mdi:information-outline", None], + "alarmdel": ["Alarm Delay", None, "mdi:alarm", None], + "ambtemp": ["Ambient Temperature", None, "mdi:thermometer", None], + "apc": ["Status Data", None, "mdi:information-outline", None], + "apcmodel": ["Model", None, "mdi:information-outline", None], + "badbatts": ["Bad Batteries", None, "mdi:information-outline", None], + "battdate": ["Battery Replaced", None, "mdi:calendar-clock", None], + "battstat": ["Battery Status", None, "mdi:information-outline", None], "battv": ["Battery Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "bcharge": ["Battery", PERCENTAGE, "mdi:battery", None], - "cable": ["Cable Type", "", "mdi:ethernet-cable", None], - "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None], - "date": ["Status Date", "", "mdi:calendar-clock", None], - "dipsw": ["Dip Switch Settings", "", "mdi:information-outline", None], - "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert", None], - "driver": ["Driver", "", "mdi:information-outline", None], - "dshutd": ["Shutdown Delay", "", "mdi:timer-outline", None], - "dwake": ["Wake Delay", "", "mdi:timer-outline", None], - "endapc": ["Date and Time", "", "mdi:calendar-clock", None], - "extbatts": ["External Batteries", "", "mdi:information-outline", None], - "firmware": ["Firmware Version", "", "mdi:information-outline", None], + "cable": ["Cable Type", None, "mdi:ethernet-cable", None], + "cumonbatt": ["Total Time on Battery", None, "mdi:timer-outline", None], + "date": ["Status Date", None, "mdi:calendar-clock", None], + "dipsw": ["Dip Switch Settings", None, "mdi:information-outline", None], + "dlowbatt": ["Low Battery Signal", None, "mdi:clock-alert", None], + "driver": ["Driver", None, "mdi:information-outline", None], + "dshutd": ["Shutdown Delay", None, "mdi:timer-outline", None], + "dwake": ["Wake Delay", None, "mdi:timer-outline", None], + "endapc": ["Date and Time", None, "mdi:calendar-clock", None], + "extbatts": ["External Batteries", None, "mdi:information-outline", None], + "firmware": ["Firmware Version", None, "mdi:information-outline", None], "hitrans": ["Transfer High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "hostname": ["Hostname", "", "mdi:information-outline", None], + "hostname": ["Hostname", None, "mdi:information-outline", None], "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None], "itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], - "lastxfer": ["Last Transfer", "", "mdi:transfer", None], - "linefail": ["Input Voltage Status", "", "mdi:information-outline", None], + "lastxfer": ["Last Transfer", None, "mdi:transfer", None], + "linefail": ["Input Voltage Status", None, "mdi:information-outline", None], "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline", None], "linev": ["Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "loadpct": ["Load", PERCENTAGE, "mdi:gauge", None], "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge", None], "lotrans": ["Transfer Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "mandate": ["Manufacture Date", "", "mdi:calendar", None], - "masterupd": ["Master Update", "", "mdi:information-outline", None], + "mandate": ["Manufacture Date", None, "mdi:calendar", None], + "masterupd": ["Master Update", None, "mdi:information-outline", None], "maxlinev": ["Input Voltage High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline", None], + "maxtime": ["Battery Timeout", None, "mdi:timer-off-outline", None], "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", None], "minlinev": ["Input Voltage Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "mintimel": ["Shutdown Time", "", "mdi:timer-outline", None], - "model": ["Model", "", "mdi:information-outline", None], + "mintimel": ["Shutdown Time", None, "mdi:timer-outline", None], + "model": ["Model", None, "mdi:information-outline", None], "nombattv": ["Battery Nominal Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "nominv": ["Nominal Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "nomoutv": ["Nominal Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash", None], "nomapnt": ["Nominal Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None], - "numxfers": ["Transfer Count", "", "mdi:counter", None], + "numxfers": ["Transfer Count", None, "mdi:counter", None], "outcurnt": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None], "outputv": ["Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None], - "reg1": ["Register 1 Fault", "", "mdi:information-outline", None], - "reg2": ["Register 2 Fault", "", "mdi:information-outline", None], - "reg3": ["Register 3 Fault", "", "mdi:information-outline", None], + "reg1": ["Register 1 Fault", None, "mdi:information-outline", None], + "reg2": ["Register 2 Fault", None, "mdi:information-outline", None], + "reg3": ["Register 3 Fault", None, "mdi:information-outline", None], "retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert", None], - "selftest": ["Last Self Test", "", "mdi:calendar-clock", None], - "sense": ["Sensitivity", "", "mdi:information-outline", None], - "serialno": ["Serial Number", "", "mdi:information-outline", None], - "starttime": ["Startup Time", "", "mdi:calendar-clock", None], - "statflag": ["Status Flag", "", "mdi:information-outline", None], - "status": ["Status", "", "mdi:information-outline", None], - "stesti": ["Self Test Interval", "", "mdi:information-outline", None], - "timeleft": ["Time Left", "", "mdi:clock-alert", None], - "tonbatt": ["Time on Battery", "", "mdi:timer-outline", None], - "upsmode": ["Mode", "", "mdi:information-outline", None], - "upsname": ["Name", "", "mdi:information-outline", None], - "version": ["Daemon Info", "", "mdi:information-outline", None], - "xoffbat": ["Transfer from Battery", "", "mdi:transfer", None], - "xoffbatt": ["Transfer from Battery", "", "mdi:transfer", None], - "xonbatt": ["Transfer to Battery", "", "mdi:transfer", None], + "selftest": ["Last Self Test", None, "mdi:calendar-clock", None], + "sense": ["Sensitivity", None, "mdi:information-outline", None], + "serialno": ["Serial Number", None, "mdi:information-outline", None], + "starttime": ["Startup Time", None, "mdi:calendar-clock", None], + "statflag": ["Status Flag", None, "mdi:information-outline", None], + "status": ["Status", None, "mdi:information-outline", None], + "stesti": ["Self Test Interval", None, "mdi:information-outline", None], + "timeleft": ["Time Left", None, "mdi:clock-alert", None], + "tonbatt": ["Time on Battery", None, "mdi:timer-outline", None], + "upsmode": ["Mode", None, "mdi:information-outline", None], + "upsname": ["Name", None, "mdi:information-outline", None], + "version": ["Daemon Info", None, "mdi:information-outline", None], + "xoffbat": ["Transfer from Battery", None, "mdi:transfer", None], + "xoffbatt": ["Transfer from Battery", None, "mdi:transfer", None], + "xonbatt": ["Transfer to Battery", None, "mdi:transfer", None], } SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} @@ -165,8 +165,7 @@ class APCUPSdSensor(SensorEntity): self.type = sensor_type self._attr_name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] self._attr_icon = SENSOR_TYPES[self.type][2] - if SENSOR_TYPES[sensor_type][1]: - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][3] def update(self): From 82b2ae8e9176806f0e8c4ffeb00fb3246b0ccf28 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 4 Aug 2021 22:16:39 +0200 Subject: [PATCH 112/199] Fix coordinator not defined in yale_smart_alarm (#53973) * Bugfix coordinator not defined * Apply suggestions from code review Co-authored-by: Martin Hjelmare --- .../yale_smart_alarm/alarm_control_panel.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index f450895f5c3..ae5596ee2e1 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -86,11 +86,12 @@ async def async_setup_entry( class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" - coordinator: YaleDataUpdateCoordinator - - _attr_name: str = coordinator.entry.data[CONF_NAME] - _attr_unique_id: str = coordinator.entry.entry_id - _identifier: str = coordinator.entry.data[CONF_USERNAME] + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: + """Initialize the Yale Alarm Device.""" + super().__init__(coordinator) + self._attr_name: str = coordinator.entry.data[CONF_NAME] + self._attr_unique_id = coordinator.entry.entry_id + self._identifier: str = coordinator.entry.data[CONF_USERNAME] @property def device_info(self) -> DeviceInfo: From ef55a8d2e25e183f9f6417895def132d93fba2e5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 4 Aug 2021 21:53:07 +0200 Subject: [PATCH 113/199] Fix divider for Fritz sensors (#53980) --- homeassistant/components/fritz/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 482d5b1d688..faf2be23164 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -61,32 +61,32 @@ def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: def _retrieve_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload transmission rate.""" - return round(status.transmission_rate[0] / 1024, 1) # type: ignore[no-any-return] + return round(status.transmission_rate[0] / 1000, 1) # type: ignore[no-any-return] def _retrieve_kb_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download transmission rate.""" - return round(status.transmission_rate[1] / 1024, 1) # type: ignore[no-any-return] + return round(status.transmission_rate[1] / 1000, 1) # type: ignore[no-any-return] def _retrieve_max_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload max transmission rate.""" - return round(status.max_bit_rate[0] / 1024, 1) # type: ignore[no-any-return] + return round(status.max_bit_rate[0] / 1000, 1) # type: ignore[no-any-return] def _retrieve_max_kb_s_received_state(status: FritzStatus, last_value: str) -> float: """Return download max transmission rate.""" - return round(status.max_bit_rate[1] / 1024, 1) # type: ignore[no-any-return] + return round(status.max_bit_rate[1] / 1000, 1) # type: ignore[no-any-return] def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload total data.""" - return round(status.bytes_sent * 8 / 1024 / 1024 / 1024, 1) # type: ignore[no-any-return] + return round(status.bytes_sent * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: """Return download total data.""" - return round(status.bytes_received * 8 / 1024 / 1024 / 1024, 1) # type: ignore[no-any-return] + return round(status.bytes_received * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] class SensorData(TypedDict, total=False): From 14621fc4456739d7d53c4c9a583a181922236e4f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 4 Aug 2021 14:57:19 -0500 Subject: [PATCH 114/199] Fix empty sonos_group entity attribute on startup (#53985) --- homeassistant/components/sonos/speaker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 2ebef334873..434717f7a85 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -654,7 +654,11 @@ class SonosSpeaker: @callback def _async_regroup(group: list[str]) -> None: """Rebuild internal group layout.""" - if group == [self.soco.uid] and self.sonos_group == [self]: + if ( + group == [self.soco.uid] + and self.sonos_group == [self] + and self.sonos_group_entities + ): # Skip updating existing single speakers in polling mode return From 72032edaeced6577c4ac3fbe3bea5e4fc462cc1d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 5 Aug 2021 02:03:04 +0200 Subject: [PATCH 115/199] Update frontend to 20210804.0 (#53997) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index fbf676687cb..b9a84cbec02 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20210803.2" - ], + "requirements": ["home-assistant-frontend==20210804.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b9fe917279..cc4bb9d72ac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210803.2 +home-assistant-frontend==20210804.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index f15f5cf66ee..11fa22976fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.2 +home-assistant-frontend==20210804.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29a8352f433..fea89dc8b57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210803.2 +home-assistant-frontend==20210804.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 436d0e0fb46f1e19742535cf731ccd70a5dde73d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Aug 2021 17:05:41 -0700 Subject: [PATCH 116/199] Bumped version to 2021.8.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b95825ae39e..65ae1a569c3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 6544a32319fc59c0d0154cb01d575d79c23a2601 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 5 Aug 2021 02:29:23 -0600 Subject: [PATCH 117/199] Bump pylitterbot to 2021.8.0 (#54000) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index a22499fb062..facf79a7bd7 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.7.2"], + "requirements": ["pylitterbot==2021.8.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 11fa22976fd..01f3ab70f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1562,7 +1562,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.7.2 +pylitterbot==2021.8.0 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fea89dc8b57..079a217bc4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.7.2 +pylitterbot==2021.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 5c9d7edaddb1422d3ab90e9d8c35965e92b360dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Aug 2021 12:12:06 +0200 Subject: [PATCH 118/199] Add missing device class to SAJ energy sensors (#54048) --- homeassistant/components/saj/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fb3b31764f8..1b46632051e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -204,6 +205,8 @@ class SAJsensor(SensorEntity): """Return the device class the sensor belongs to.""" if self.unit_of_measurement == POWER_WATT: return DEVICE_CLASS_POWER + if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: + return DEVICE_CLASS_ENERGY if ( self.unit_of_measurement == TEMP_CELSIUS or self._sensor.unit == TEMP_FAHRENHEIT From bc548050f18cbb2e54d783d391bcac2d93c28467 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Aug 2021 12:47:14 -0500 Subject: [PATCH 119/199] Handle empty software version when setting up HomeKit (#54068) Fixes #54059 Fixes #54024 --- .../components/homekit/accessories.py | 8 +++--- tests/components/homekit/test_accessories.py | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 49ba1103ac5..772143e7c1f 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -228,17 +228,17 @@ class HomeAccessory(Accessory): self.config = config or {} domain = split_entity_id(entity_id)[0].replace("_", " ") - if ATTR_MANUFACTURER in self.config: + if self.config.get(ATTR_MANUFACTURER) is not None: manufacturer = self.config[ATTR_MANUFACTURER] - elif ATTR_INTEGRATION in self.config: + elif self.config.get(ATTR_INTEGRATION) is not None: manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title() else: manufacturer = f"{MANUFACTURER} {domain}".title() - if ATTR_MODEL in self.config: + if self.config.get(ATTR_MODEL) is not None: model = self.config[ATTR_MODEL] else: model = domain.title() - if ATTR_SOFTWARE_VERSION in self.config: + if self.config.get(ATTR_SOFTWARE_VERSION) is not None: sw_version = format_sw_version(self.config[ATTR_SOFTWARE_VERSION]) else: sw_version = __version__ diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 257be293fc0..32397335133 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -41,6 +41,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, __version__, + __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -130,6 +131,7 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == "0.4.3" hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -157,6 +159,31 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_MODEL).value == "Test Model" +async def test_accessory_with_missing_basic_service_info(hass, hk_driver): + """Test HomeAccessory class.""" + entity_id = "sensor.accessory" + hass.states.async_set(entity_id, "on") + acc = HomeAccessory( + hass, + hk_driver, + "Home Accessory", + entity_id, + 3, + { + ATTR_MODEL: None, + ATTR_MANUFACTURER: None, + ATTR_SW_VERSION: None, + ATTR_INTEGRATION: None, + }, + ) + serv = acc.get_service(SERV_ACCESSORY_INFO) + assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" + assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + + async def test_battery_service(hass, hk_driver, caplog): """Test battery service.""" entity_id = "homekit.accessory" From fbfb77b900fdd68d94fd09227decadee0365b524 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 Aug 2021 13:46:58 -0400 Subject: [PATCH 120/199] Bump up ZHA dependencies (#54079) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fa117a3f1ff..5200c0a8b31 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,11 +9,11 @@ "pyserial-asyncio==0.5", "zha-quirks==0.0.59", "zigpy-cc==0.5.2", - "zigpy-deconz==0.12.0", + "zigpy-deconz==0.12.1", "zigpy==0.36.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.5.2" + "zigpy-znp==0.5.3" ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 01f3ab70f21..c98c1be2dae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.12.0 +zigpy-deconz==0.12.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2463,7 +2463,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.2 +zigpy-znp==0.5.3 # homeassistant.components.zha zigpy==0.36.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 079a217bc4b..73bb775bbbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1350,7 +1350,7 @@ zha-quirks==0.0.59 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.12.0 +zigpy-deconz==0.12.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1359,7 +1359,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.2 +zigpy-znp==0.5.3 # homeassistant.components.zha zigpy==0.36.1 From 557e1862d50d10839e4174312b4127c56ec09d1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Aug 2021 13:11:01 -0700 Subject: [PATCH 121/199] Packages to support config platforms (#54085) --- homeassistant/components/automation/__init__.py | 3 --- homeassistant/components/automation/config.py | 2 ++ homeassistant/components/script/config.py | 2 ++ homeassistant/components/template/config.py | 2 ++ homeassistant/config.py | 17 ++++++++++++++++- tests/test_config.py | 13 ++++++++++++- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a0ff4930b51..5e1b53c535e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -71,9 +71,6 @@ from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime from .config import AutomationConfig, async_validate_config_item - -# Not used except by packages to check config structure -from .config import PLATFORM_SCHEMA # noqa: F401 from .const import ( CONF_ACTION, CONF_INITIAL_STATE, diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 83076778b91..e852b6cc4c0 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -37,6 +37,8 @@ from .helpers import async_get_blueprints # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any +PACKAGE_MERGE_HINT = "list" + _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 6993b7181e1..44b739e84c7 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -39,6 +39,8 @@ from .const import ( ) from .helpers import async_get_blueprints +PACKAGE_MERGE_HINT = "dict" + SCRIPT_ENTITY_SCHEMA = make_script_schema( { vol.Optional(CONF_ALIAS): cv.string, diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 007f40a6d0a..165420bf404 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -13,6 +13,8 @@ from homeassistant.helpers.trigger import async_validate_trigger_config from . import binary_sensor as binary_sensor_platform, sensor as sensor_platform from .const import CONF_TRIGGER, DOMAIN +PACKAGE_MERGE_HINT = "list" + CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/config.py b/homeassistant/config.py index e7b6e04e8cf..12a39ab291b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -723,7 +723,22 @@ async def merge_packages_config( _log_pkg_error(pack_name, comp_name, config, str(ex)) continue - merge_list = hasattr(component, "PLATFORM_SCHEMA") + try: + config_platform: ModuleType | None = integration.get_platform("config") + # Test if config platform has a config validator + if not hasattr(config_platform, "async_validate_config"): + config_platform = None + except ImportError: + config_platform = None + + merge_list = False + + # If integration has a custom config validator, it needs to provide a hint. + if config_platform is not None: + merge_list = config_platform.PACKAGE_MERGE_HINT == "list" # type: ignore[attr-defined] + + if not merge_list: + merge_list = hasattr(component, "PLATFORM_SCHEMA") if not merge_list and hasattr(component, "CONFIG_SCHEMA"): merge_list = _identify_config_schema(component) == "list" diff --git a/tests/test_config.py b/tests/test_config.py index c1eb1ab7540..96196c943aa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -650,19 +650,30 @@ async def test_merge(merge_log_err, hass): "pack_list": {"light": {"platform": "test"}}, "pack_list2": {"light": [{"platform": "test"}]}, "pack_none": {"wake_on_lan": None}, + "pack_special": { + "automation": [{"some": "yay"}], + "script": {"a_script": "yay"}, + "template": [{"some": "yay"}], + }, } config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, + "automation": [], + "script": {}, + "template": [], } await config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 - assert len(config) == 5 + assert len(config) == 8 assert len(config["input_boolean"]) == 2 assert len(config["input_select"]) == 1 assert len(config["light"]) == 3 + assert len(config["automation"]) == 1 + assert len(config["script"]) == 1 + assert len(config["template"]) == 1 assert isinstance(config["wake_on_lan"], OrderedDict) From 52f7f203d24d9eafc5d1e7988610b98318926e0a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 6 Aug 2021 05:23:05 +0300 Subject: [PATCH 122/199] Fix Shelly last_reset (#54101) --- homeassistant/components/shelly/entity.py | 1 - homeassistant/components/shelly/sensor.py | 52 ++++++++++++++++------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index a1ce2e671d1..0d23f5abffc 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -285,7 +285,6 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit: None | str | Callable[[dict], str] = unit self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) - self._last_value: str | None = None @property def unique_id(self) -> str: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 56e4f63bc75..07e4f4a4fe3 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,9 +1,12 @@ """Sensor for Shelly.""" from __future__ import annotations -from datetime import datetime +from datetime import timedelta +import logging from typing import Final, cast +import aioshelly + from homeassistant.components import sensor from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -23,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt +from . import ShellyDeviceWrapper from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, @@ -35,6 +39,8 @@ from .entity import ( ) from .utils import get_device_uptime, temperature_unit +_LOGGER: Final = logging.getLogger(__name__) + SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", @@ -255,9 +261,39 @@ async def async_setup_entry( class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + description: BlockAttributeDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block, attribute, description) + self._last_value: float | None = None + + if description.last_reset == LAST_RESET_NEVER: + self._attr_last_reset = dt.utc_from_timestamp(0) + elif description.last_reset == LAST_RESET_UPTIME: + self._attr_last_reset = ( + dt.utcnow() - timedelta(seconds=wrapper.device.status["uptime"]) + ).replace(second=0, microsecond=0) + @property def state(self) -> StateType: """Return value of sensor.""" + if ( + self.description.last_reset == LAST_RESET_UPTIME + and self.attribute_value is not None + ): + value = cast(float, self.attribute_value) + + if self._last_value and self._last_value > value: + self._attr_last_reset = dt.utcnow().replace(second=0, microsecond=0) + _LOGGER.info("Energy reset detected for entity %s", self.name) + + self._last_value = value + return self.attribute_value @property @@ -265,20 +301,6 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """State class of sensor.""" return self.description.state_class - @property - def last_reset(self) -> datetime | None: - """State class of sensor.""" - if self.description.last_reset == LAST_RESET_UPTIME: - self._last_value = get_device_uptime( - self.wrapper.device.status, self._last_value - ) - return dt.parse_datetime(self._last_value) - - if self.description.last_reset == LAST_RESET_NEVER: - return dt.utc_from_timestamp(0) - - return None - @property def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" From e37bf733bd72909bade8a03170a23eec413dcbe1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 6 Aug 2021 04:24:41 +0200 Subject: [PATCH 123/199] Two fixes (#54102) --- homeassistant/components/fritz/sensor.py | 4 ++-- homeassistant/components/fritz/switch.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index faf2be23164..d7a34564b43 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -81,12 +81,12 @@ def _retrieve_max_kb_s_received_state(status: FritzStatus, last_value: str) -> f def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload total data.""" - return round(status.bytes_sent * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_sent / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: """Return download total data.""" - return round(status.bytes_received * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_received / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] class SensorData(TypedDict, total=False): diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 10eb6553dbd..da17bef7159 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -13,7 +13,6 @@ from fritzconnection.core.exceptions import ( FritzSecurityError, FritzServiceError, ) -import slugify as unicode_slug import xmltodict from homeassistant.components.network import async_get_source_ip @@ -248,10 +247,18 @@ def wifi_entities_list( ) if network_info: ssid = network_info["NewSSID"] - if unicode_slug.slugify(ssid, lowercase=False) in networks.values(): + _LOGGER.debug("SSID from device: <%s>", ssid) + if ( + slugify( + ssid, + ) + in [slugify(v) for v in networks.values()] + ): + _LOGGER.debug("SSID duplicated, adding suffix") networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' else: networks[i] = ssid + _LOGGER.debug("SSID normalized: <%s>", networks[i]) return [ FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, network_name) From fa4ec926ce9ddb3a0854fca018c51c90757b2da3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Aug 2021 21:24:24 -0500 Subject: [PATCH 124/199] Increase time before scene and script HomeKit entities are reset (#54105) --- homeassistant/components/homekit/type_switches.py | 4 +++- tests/components/homekit/test_type_switches.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 381110a4e79..ef9dadff287 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -57,6 +57,8 @@ VALVE_TYPE = { ACTIVATE_ONLY_SWITCH_DOMAINS = {"scene", "script"} +ACTIVATE_ONLY_RESET_SECONDS = 10 + @TYPES.register("Outlet") class Outlet(HomeAccessory): @@ -141,7 +143,7 @@ class Switch(HomeAccessory): self.async_call_service(self._domain, service, params) if self.activate_only: - async_call_later(self.hass, 1, self.reset_switch) + async_call_later(self.hass, ACTIVATE_ONLY_RESET_SECONDS, self.reset_switch) @callback def async_update_state(self, new_state): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 455f7a6141a..6df1f0182ed 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -329,7 +329,13 @@ async def test_reset_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off @@ -367,7 +373,13 @@ async def test_script_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off From 374ccaae4784f0f1e94668ac52ccec97ccff09e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Aug 2021 21:24:09 -0500 Subject: [PATCH 125/199] Bump zeroconf to 0.33.3 (#54108) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ee1e9a8e1ab..7b3cfa1fefd 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.2"], + "requirements": ["zeroconf==0.33.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc4bb9d72ac..d435f165f61 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.2 +zeroconf==0.33.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index c98c1be2dae..248a33ee293 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73bb775bbbc..0ec7f8b25f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 From b00173705ec6a3e35dad607e8b1b89ca60ba046a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Aug 2021 20:49:03 -0700 Subject: [PATCH 126/199] Bumped version to 2021.8.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 65ae1a569c3..b8134905927 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -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, 8, 0) From 890d190612eda52d8d10a37a7a29f6555f45ddcb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Aug 2021 23:15:20 -0700 Subject: [PATCH 127/199] Fix constant --- tests/components/homekit/test_accessories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 32397335133..00e894b91cb 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -172,7 +172,7 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): { ATTR_MODEL: None, ATTR_MANUFACTURER: None, - ATTR_SW_VERSION: None, + ATTR_SOFTWARE_VERSION: None, ATTR_INTEGRATION: None, }, ) From 07f8236e6f2920ab8bd9c531b81bf2735331ed87 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Fri, 6 Aug 2021 03:18:29 -0700 Subject: [PATCH 128/199] Gracefully handle additional GSM errors (#54114) --- homeassistant/components/sms/gateway.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 51667ef8f77..5003f7019ca 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -25,6 +25,10 @@ class Gateway: await self._worker.set_incoming_sms_async() except gammu.ERR_NOTSUPPORTED: _LOGGER.warning("Your phone does not support incoming SMS notifications!") + except gammu.GSMError: + _LOGGER.warning( + "GSM error, your phone does not support incoming SMS notifications!" + ) else: await self._worker.set_incoming_callback_async(self.sms_callback) From 93a2e2849c57cd4223620dd70cc9693b6b655fc4 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Fri, 6 Aug 2021 17:34:42 +0100 Subject: [PATCH 129/199] Handle software version being None when setting up HomeKit accessories (#54130) * Convert all HomeKit service info to string prior to checking for max length * Added check for None software version * Added test case for numeric version number * Update tests/components/homekit/test_accessories.py Co-authored-by: J. Nick Koston * Fix style & none version test * Fix test * revert other change since it should be covered by the format_sw_version fix Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 3 +- tests/components/homekit/test_accessories.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 772143e7c1f..836221ac7e9 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -238,9 +238,10 @@ class HomeAccessory(Accessory): model = self.config[ATTR_MODEL] else: model = domain.title() + sw_version = None if self.config.get(ATTR_SOFTWARE_VERSION) is not None: sw_version = format_sw_version(self.config[ATTR_SOFTWARE_VERSION]) - else: + if sw_version is None: sw_version = __version__ self.set_info_service( diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 00e894b91cb..0584ee4c0ff 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -133,6 +133,39 @@ async def test_home_accessory(hass, hk_driver): ) assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == "0.4.3" + acc4 = HomeAccessory( + hass, + hk_driver, + "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length", + entity_id2, + 3, + { + ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_SOFTWARE_VERSION: "will_not_match_regex", + ATTR_INTEGRATION: "luxe that exceeds the maximum maximum maximum maximum maximum maximum length", + }, + ) + assert acc4.available is False + serv = acc4.services[0] # SERV_ACCESSORY_INFO + assert ( + serv.get_characteristic(CHAR_NAME).value + == "Home Accessory that exceeds the maximum maximum maximum maximum " + ) + assert ( + serv.get_characteristic(CHAR_MANUFACTURER).value + == "Lux Brands that exceeds the maximum maximum maximum maximum maxi" + ) + assert ( + serv.get_characteristic(CHAR_MODEL).value + == "Awesome Model that exceeds the maximum maximum maximum maximum m" + ) + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( From 15e9310a01cfe699caf33b7dad21bf983fb5d86e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 6 Aug 2021 14:59:00 +0200 Subject: [PATCH 130/199] Fix sensor PLATFORM_SCHEMA for ebox and enphase_envoy (#54142) * Fix sensor PLATFORM_SCHEMA * fix pylint --- homeassistant/components/ebox/sensor.py | 4 +++- homeassistant/components/enphase_envoy/sensor.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index e27c6fe0772..e98dea45929 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -122,10 +122,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) +SENSOR_TYPE_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(SENSOR_TYPE_KEYS)] ), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 29d273401f4..3af5cd1ec0c 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,4 +1,5 @@ """Support for Enphase Envoy solar energy monitor.""" +from __future__ import annotations import logging @@ -22,14 +23,15 @@ ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" _LOGGER = logging.getLogger(__name__) +SENSOR_KEYS: list[str] = [desc.key for desc in SENSORS] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, vol.Optional(CONF_USERNAME, default="envoy"): cv.string, vol.Optional(CONF_PASSWORD, default=""): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(list(SENSORS))] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ), vol.Optional(CONF_NAME, default=""): cv.string, } From d580036dfbd48e208fb25889def73fa306d97b72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Aug 2021 11:15:35 -0500 Subject: [PATCH 131/199] Fetch interface index from network integration instead of socket.if_nametoindex in zeroconf (#54152) --- homeassistant/components/network/models.py | 1 + homeassistant/components/network/util.py | 1 + homeassistant/components/zeroconf/__init__.py | 5 ++- tests/components/network/test_init.py | 32 +++++++++++++++++++ tests/components/zeroconf/test_init.py | 4 +++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/network/models.py b/homeassistant/components/network/models.py index a007eb8636d..d3fbc824489 100644 --- a/homeassistant/components/network/models.py +++ b/homeassistant/components/network/models.py @@ -24,6 +24,7 @@ class Adapter(TypedDict): """Configured network adapters.""" name: str + index: int enabled: bool auto: bool default: bool diff --git a/homeassistant/components/network/util.py b/homeassistant/components/network/util.py index eece4b38548..f8b33b3df90 100644 --- a/homeassistant/components/network/util.py +++ b/homeassistant/components/network/util.py @@ -116,6 +116,7 @@ def _ifaddr_adapter_to_ha( return { "name": adapter.nice_name, + "index": adapter.index, "enabled": False, "auto": auto, "default": default, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 4c4c81aff32..cdb46318578 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -155,9 +155,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: for ipv4 in ipv4s if not ipaddress.ip_address(ipv4["address"]).is_loopback ) - if adapter["ipv6"]: - ifi = socket.if_nametoindex(adapter["name"]) - interfaces.append(ifi) + if adapter["ipv6"] and adapter["index"] not in interfaces: + interfaces.append(adapter["index"]) ipv6 = True if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index bc4c543842f..6a85f5ea9e8 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -21,15 +21,19 @@ def _generate_mock_adapters(): mock_lo0 = Mock(spec=ifaddr.Adapter) mock_lo0.nice_name = "lo0" mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_lo0.index = 0 mock_eth0 = Mock(spec=ifaddr.Adapter) mock_eth0.nice_name = "eth0" mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth0.index = 1 mock_eth1 = Mock(spec=ifaddr.Adapter) mock_eth1.nice_name = "eth1" mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] + mock_eth1.index = 2 mock_vtun0 = Mock(spec=ifaddr.Adapter) mock_vtun0.nice_name = "vtun0" mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] + mock_vtun0.index = 3 return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] @@ -51,6 +55,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto assert network_obj.adapters == [ { "auto": False, + "index": 1, "default": False, "enabled": False, "ipv4": [], @@ -65,6 +70,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "eth0", }, { + "index": 0, "auto": False, "default": False, "enabled": False, @@ -73,6 +79,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "lo0", }, { + "index": 2, "auto": True, "default": True, "enabled": True, @@ -81,6 +88,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "eth1", }, { + "index": 3, "auto": False, "default": False, "enabled": False, @@ -107,6 +115,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { + "index": 1, "auto": True, "default": False, "enabled": True, @@ -122,6 +131,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "eth0", }, { + "index": 0, "auto": False, "default": True, "enabled": False, @@ -130,6 +140,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "lo0", }, { + "index": 2, "auto": True, "default": False, "enabled": True, @@ -138,6 +149,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "eth1", }, { + "index": 3, "auto": False, "default": False, "enabled": False, @@ -165,6 +177,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): assert network_obj.adapters == [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -180,6 +193,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -188,6 +202,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -196,6 +211,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -222,6 +238,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): assert network_obj.adapters == [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -237,6 +254,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -245,6 +263,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -253,6 +272,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -285,6 +305,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): assert network_obj.adapters == [ { "auto": False, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -300,6 +321,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -308,6 +330,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -316,6 +339,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -356,6 +380,7 @@ async def test_interfaces_configured_from_storage_websocket_update( assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -371,6 +396,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -379,6 +405,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -387,6 +414,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -407,6 +435,7 @@ async def test_interfaces_configured_from_storage_websocket_update( assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, + "index": 1, "default": False, "enabled": False, "ipv4": [], @@ -422,6 +451,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -430,6 +460,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -438,6 +469,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 3b8cf883a13..e1e346621fe 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -725,6 +725,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route( _ADAPTERS_WITH_MANUAL_CONFIG = [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -746,6 +747,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -754,6 +756,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": True, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "172.16.1.5", "network_prefix": 23}], @@ -769,6 +772,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": False, + "index": 4, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], From 011cda5fc4ff9c030a8b4ca972f4dc0d9cc51f45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Aug 2021 14:48:00 -0500 Subject: [PATCH 132/199] Bump zeroconf to 0.33.4 to ensure zeroconf can startup when ipv6 is disabled (#54165) Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.33.3...0.33.4 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 7b3cfa1fefd..1847a1c806b 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.3"], + "requirements": ["zeroconf==0.33.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d435f165f61..617a057743b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.3 +zeroconf==0.33.4 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 248a33ee293..8c936677ba2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.3 +zeroconf==0.33.4 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ec7f8b25f4..f9fbc43d789 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.3 +zeroconf==0.33.4 # homeassistant.components.zha zha-quirks==0.0.59 From cb775029e0ae74ebb7daa0e2cbecb66a18aa5db6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Aug 2021 12:55:59 -0700 Subject: [PATCH 133/199] Bumped version to 2021.8.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b8134905927..d4a79d3a8bb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 4fbe7130791c2c7145f6d5052a67062011558ccb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 8 Aug 2021 05:55:01 +0200 Subject: [PATCH 134/199] Add missing `motor_speed` sensor for Xiaomi Miio humidifier CA1 and CB1 (#54202) --- .../components/xiaomi_miio/sensor.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 413971aa880..9804e0298bc 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -45,6 +45,8 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, MODELS_HUMIDIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity @@ -63,16 +65,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ATTR_POWER = "power" +ATTR_ACTUAL_SPEED = "actual_speed" ATTR_CHARGING = "charging" ATTR_DISPLAY_CLOCK = "display_clock" +ATTR_HUMIDITY = "humidity" +ATTR_MOTOR_SPEED = "motor_speed" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" +ATTR_POWER = "power" ATTR_SENSOR_STATE = "sensor_state" ATTR_WATER_LEVEL = "water_level" -ATTR_HUMIDITY = "humidity" -ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" @dataclass @@ -121,6 +124,13 @@ SENSOR_TYPES = { valid_min_value=200.0, valid_max_value=2000.0, ), + "motor_speed": SensorType( + unit="rpm", + icon="mdi:fast-forward", + state_class=STATE_CLASS_MEASUREMENT, + valid_min_value=200.0, + valid_max_value=2000.0, + ), } HUMIDIFIER_SENSORS = { @@ -128,11 +138,17 @@ HUMIDIFIER_SENSORS = { ATTR_TEMPERATURE: "temperature", } +HUMIDIFIER_CA1_CB1_SENSORS = { + ATTR_HUMIDITY: "humidity", + ATTR_TEMPERATURE: "temperature", + ATTR_MOTOR_SPEED: "motor_speed", +} + HUMIDIFIER_SENSORS_MIOT = { ATTR_HUMIDITY: "humidity", ATTR_TEMPERATURE: "temperature", ATTR_WATER_LEVEL: "water_level", - ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", + ATTR_ACTUAL_SPEED: "actual_speed", } @@ -191,11 +207,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] if model in MODELS_HUMIDIFIER_MIOT: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] sensors = HUMIDIFIER_SENSORS_MIOT + elif model in (MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1): + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + sensors = HUMIDIFIER_CA1_CB1_SENSORS elif model.startswith("zhimi.humidifier."): device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] sensors = HUMIDIFIER_SENSORS else: unique_id = config_entry.unique_id From 1934159fd0f9ab3bf8e0eb43ed43b865533c9115 Mon Sep 17 00:00:00 2001 From: Trinnik Date: Sat, 7 Aug 2021 21:51:05 -0600 Subject: [PATCH 135/199] Fix update entity prior to adding (#54015) --- homeassistant/components/aladdin_connect/cover.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 85f89f3043b..14e2b2f0ce2 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -49,7 +49,10 @@ def setup_platform( try: if not acc.login(): raise ValueError("Username or Password is incorrect") - add_entities(AladdinDevice(acc, door) for door in acc.get_doors()) + add_entities( + (AladdinDevice(acc, door) for door in acc.get_doors()), + update_before_add = True + ) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) hass.components.persistent_notification.create( From 3a17e22982e4b26dfe3e8a1c01656ab94b70896a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 7 Aug 2021 01:18:08 -0400 Subject: [PATCH 136/199] Fix androidtv media_image_hash (#54188) --- homeassistant/components/androidtv/media_player.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 98d1ac0ae18..4d87ebc2592 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -449,6 +449,11 @@ class ADBDevice(MediaPlayerEntity): ATTR_HDMI_INPUT: None, } + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" if self._screencap else None + @adb_decorator() async def _adb_screencap(self): """Take a screen capture from the device.""" @@ -458,9 +463,6 @@ class ADBDevice(MediaPlayerEntity): """Fetch current playing image.""" if not self._screencap or self.state in [STATE_OFF, None] or not self.available: return None, None - self._attr_media_image_hash = ( - f"{datetime.now().timestamp()}" if self._screencap else None - ) media_data = await self._adb_screencap() if media_data: From 94e26df6d373b828468d00ab2e72e0b02bde7fdf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 06:11:56 +0200 Subject: [PATCH 137/199] Solve missing automatic update of struct configuration in modbus (#54193) --- homeassistant/components/modbus/validators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 9d72b611adc..f7fdae0a82a 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -72,6 +72,7 @@ def struct_validator(config): _LOGGER.warning(error) try: data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)] + config[CONF_DATA_TYPE] = data_type except KeyError as exp: error = f"{name} cannot convert automatically {data_type}" raise vol.Invalid(error) from exp From 56d0ef34fd83067a862de1477d9057d32b15b305 Mon Sep 17 00:00:00 2001 From: Mk4242 <76903406+Mk4242@users.noreply.github.com> Date: Sat, 7 Aug 2021 18:24:19 +0200 Subject: [PATCH 138/199] Update const.py (#54195) Remove extra attribute for FlowTemperature sensor, which prevents the ebusd integration from initialising --- homeassistant/components/ebusd/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 7052a9950fd..3d4ab508ca2 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -223,7 +223,6 @@ SENSOR_TYPES = { None, 4, DEVICE_CLASS_TEMPERATURE, - None, ], "Flame": ["Flame", None, "mdi:toggle-switch", 2, None], "PowerEnergyConsumptionHeatingCircuit": [ From 5dcf5edae0926e06a0713ad03674420f33a52b3e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 06:10:08 +0200 Subject: [PATCH 139/199] Add parameter to delay sending of requests in modbus (#54203) --- homeassistant/components/modbus/__init__.py | 2 ++ homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/modbus.py | 11 +++++++++-- tests/components/modbus/test_init.py | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 16be39230db..43aa49e6da7 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -66,6 +66,7 @@ from .const import ( CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_MSG_WAIT, CONF_PARITY, CONF_PRECISION, CONF_RETRIES, @@ -283,6 +284,7 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_DELAY, default=0): cv.positive_int, vol.Optional(CONF_RETRIES, default=3): cv.positive_int, vol.Optional(CONF_RETRY_ON_EMPTY, default=False): cv.boolean, + vol.Optional(CONF_MSG_WAIT): cv.positive_int, vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] ), diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 49b7683435e..c5d182cdf4a 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -31,6 +31,7 @@ CONF_INPUTS = "inputs" CONF_INPUT_TYPE = "input_type" CONF_MAX_TEMP = "max_temp" CONF_MIN_TEMP = "min_temp" +CONF_MSG_WAIT = "message_wait_milliseconds" CONF_PARITY = "parity" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 8d2ea46e293..1cb68aa4fd6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -39,6 +39,7 @@ from .const import ( CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLOSE_COMM_ON_ERROR, + CONF_MSG_WAIT, CONF_PARITY, CONF_RETRIES, CONF_RETRY_ON_EMPTY, @@ -229,6 +230,12 @@ class ModbusHub: self._pb_params["framer"] = ModbusRtuFramer Defaults.Timeout = client_config[CONF_TIMEOUT] + if CONF_MSG_WAIT in client_config: + self._msg_wait = client_config[CONF_MSG_WAIT] / 1000 + elif self._config_type == CONF_SERIAL: + self._msg_wait = 30 / 1000 + else: + self._msg_wait = 0 def _log_error(self, text: str, error_state=True): log_text = f"Pymodbus: {text}" @@ -322,7 +329,7 @@ class ModbusHub: result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) - if self._config_type == "serial": + if self._msg_wait: # small delay until next request/response - await asyncio.sleep(30 / 1000) + await asyncio.sleep(self._msg_wait) return result diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8b8d063bf02..b9f6420604f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -40,6 +40,7 @@ from homeassistant.components.modbus.const import ( CONF_BYTESIZE, CONF_DATA_TYPE, CONF_INPUT_TYPE, + CONF_MSG_WAIT, CONF_PARITY, CONF_STOPBITS, CONF_SWAP, @@ -245,6 +246,7 @@ async def test_exception_struct_validator(do_config): CONF_PORT: "usb01", CONF_PARITY: "E", CONF_STOPBITS: 1, + CONF_MSG_WAIT: 100, }, { CONF_TYPE: "serial", From 13ded1e5b28befb491e61594c1efd1e4df09343a Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sun, 8 Aug 2021 06:03:20 +0200 Subject: [PATCH 140/199] Bugfix: Bring back unique IDs for ADS covers after #52488 (#54212) --- homeassistant/components/ads/cover.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index 0cd0264cb50..976bfd58fed 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -91,13 +91,13 @@ class AdsCover(AdsEntity, CoverEntity): ): """Initialize AdsCover entity.""" super().__init__(ads_hub, name, ads_var_is_closed) - if self._ads_var is None: + if self._attr_unique_id is None: if ads_var_position is not None: - self._unique_id = ads_var_position + self._attr_unique_id = ads_var_position elif ads_var_pos_set is not None: - self._unique_id = ads_var_pos_set + self._attr_unique_id = ads_var_pos_set elif ads_var_open is not None: - self._unique_id = ads_var_open + self._attr_unique_id = ads_var_open self._state_dict[STATE_KEY_POSITION] = None self._ads_var_position = ads_var_position From 724f11bb0d27815acea80f464d709ca3adf0bb4d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 7 Aug 2021 21:29:52 -0700 Subject: [PATCH 141/199] Don't block motionEye setup on NoURLAvailableError (#54225) Co-authored-by: Paulus Schoutsen --- .../components/motioneye/__init__.py | 68 +++++++++++-------- tests/components/motioneye/test_web_hooks.py | 31 +++++++++ 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 2ade7c48e1b..acafdceeb05 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -53,7 +53,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -145,12 +145,21 @@ def listen_for_new_cameras( @callback -def async_generate_motioneye_webhook(hass: HomeAssistant, webhook_id: str) -> str: +def async_generate_motioneye_webhook( + hass: HomeAssistant, webhook_id: str +) -> str | None: """Generate the full local URL for a webhook_id.""" - return "{}{}".format( - get_url(hass, allow_cloud=False), - async_generate_path(webhook_id), - ) + try: + return "{}{}".format( + get_url(hass, allow_cloud=False), + async_generate_path(webhook_id), + ) + except NoURLAvailableError: + _LOGGER.warning( + "Unable to get Home Assistant URL. Have you set the internal and/or " + "external URLs in Configuration -> General?" + ) + return None @callback @@ -228,28 +237,31 @@ def _add_camera( if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): url = async_generate_motioneye_webhook(hass, entry.data[CONF_WEBHOOK_ID]) - if _set_webhook( - _build_url( - device, - url, - EVENT_MOTION_DETECTED, - EVENT_MOTION_DETECTED_KEYS, - ), - KEY_WEB_HOOK_NOTIFICATIONS_URL, - KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, - KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, - camera, - ) | _set_webhook( - _build_url( - device, - url, - EVENT_FILE_STORED, - EVENT_FILE_STORED_KEYS, - ), - KEY_WEB_HOOK_STORAGE_URL, - KEY_WEB_HOOK_STORAGE_HTTP_METHOD, - KEY_WEB_HOOK_STORAGE_ENABLED, - camera, + if url and ( + _set_webhook( + _build_url( + device, + url, + EVENT_MOTION_DETECTED, + EVENT_MOTION_DETECTED_KEYS, + ), + KEY_WEB_HOOK_NOTIFICATIONS_URL, + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, + KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, + camera, + ) + | _set_webhook( + _build_url( + device, + url, + EVENT_FILE_STORED, + EVENT_FILE_STORED_KEYS, + ), + KEY_WEB_HOOK_STORAGE_URL, + KEY_WEB_HOOK_STORAGE_HTTP_METHOD, + KEY_WEB_HOOK_STORAGE_ENABLED, + camera, + ) ): hass.async_create_task(client.async_set_camera(camera_id, camera)) diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index 03b4e8bc46a..f20ef5101e9 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -32,11 +32,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component from . import ( TEST_CAMERA, TEST_CAMERA_DEVICE_IDENTIFIER, + TEST_CAMERA_ENTITY_ID, TEST_CAMERA_ID, TEST_CAMERA_NAME, TEST_CAMERAS, @@ -251,6 +253,35 @@ async def test_setup_camera_with_correct_webhook( assert not client.async_set_camera.called +async def test_setup_camera_with_no_home_assistant_urls( + hass: HomeAssistant, + caplog: Any, +) -> None: + """Verify setup works without Home Assistant internal/external URLs.""" + + client = create_mock_motioneye_client() + config_entry = create_mock_motioneye_config_entry(hass, data={CONF_URL: TEST_URL}) + + with patch( + "homeassistant.components.motioneye.get_url", side_effect=NoURLAvailableError + ): + await setup_mock_motioneye_config_entry( + hass, + config_entry=config_entry, + client=client, + ) + + # Should log a warning ... + assert "Unable to get Home Assistant URL" in caplog.text + + # ... should not set callbacks in the camera ... + assert not client.async_set_camera.called + + # ... but camera should still be present. + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + + async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: """Test good callbacks.""" await async_setup_component(hass, "http", {"http": {}}) From fb6aca4f8bfe15b471e04025af4d5ec96a02f49c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Aug 2021 21:00:37 -0700 Subject: [PATCH 142/199] Pin google-cloud-pubsub to an older version (#54239) Pin google-cloud-pubsub to an older version, since newer versions have a pin that is incompatible with the existing grpcio pin already in package_constraints.txt --- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 617a057743b..6d22aa51b24 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -51,6 +51,11 @@ httplib2>=0.19.0 # https://github.com/home-assistant/core/issues/40148 grpcio==1.31.0 +# Newer versions of cloud pubsub pin a higher version of grpcio. This can +# be reverted when the grpcio pin is reverted, see: +# https://github.com/home-assistant/core/issues/53427 +google-cloud-pubsub==2.1.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7dcc4f71fe8..934ea9be90c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,11 @@ httplib2>=0.19.0 # https://github.com/home-assistant/core/issues/40148 grpcio==1.31.0 +# Newer versions of cloud pubsub pin a higher version of grpcio. This can +# be reverted when the grpcio pin is reverted, see: +# https://github.com/home-assistant/core/issues/53427 +google-cloud-pubsub==2.1.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From f458f330a53c6144360793210ce32350c3cd2250 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 Aug 2021 21:34:56 -0700 Subject: [PATCH 143/199] Bumped version to 2021.8.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d4a79d3a8bb..4c7ab9742b0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From fe9808926e4bf10867f3f25d1e71e53ff8e4566a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 Aug 2021 21:10:21 -0700 Subject: [PATCH 144/199] Fix formatting (#54247) --- homeassistant/components/aladdin_connect/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 14e2b2f0ce2..5cebe3622dc 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -51,7 +51,7 @@ def setup_platform( raise ValueError("Username or Password is incorrect") add_entities( (AladdinDevice(acc, door) for door in acc.get_doors()), - update_before_add = True + update_before_add=True, ) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) From 903e2243e7502e12af8516973683873e0e523fbc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 8 Aug 2021 09:43:08 -0400 Subject: [PATCH 145/199] Fix camera state and attributes for agent_dvr (#54049) * Fix camera state and attributes for agent_dvr * tweak * tweak --- homeassistant/components/agent_dvr/camera.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 30c27eb047a..8a29428a833 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -67,8 +67,6 @@ async def async_setup_entry( class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" - _attr_supported_features = SUPPORT_ON_OFF - def __init__(self, device): """Initialize as a subclass of MjpegCamera.""" device_info = { @@ -80,7 +78,6 @@ class AgentCamera(MjpegCamera): self._removed = False self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" - self._attr_should_poll = True super().__init__(device_info) self._attr_device_info = { "identifiers": {(AGENT_DOMAIN, self.unique_id)}, @@ -102,10 +99,10 @@ class AgentCamera(MjpegCamera): if self.device.client.is_available and not self._removed: _LOGGER.error("%s lost", self.name) self._removed = True - self._attr_available = self.device.client.is_available self._attr_icon = "mdi:camcorder-off" if self.is_on: self._attr_icon = "mdi:camcorder" + self._attr_available = self.device.client.is_available self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, "editable": False, @@ -117,6 +114,11 @@ class AgentCamera(MjpegCamera): "alerts_enabled": self.device.alerts_active, } + @property + def should_poll(self) -> bool: + """Update the state periodically.""" + return True + @property def is_recording(self) -> bool: """Return whether the monitor is recording.""" @@ -137,6 +139,11 @@ class AgentCamera(MjpegCamera): """Return True if entity is connected.""" return self.device.connected + @property + def supported_features(self) -> int: + """Return supported features.""" + return SUPPORT_ON_OFF + @property def is_on(self) -> bool: """Return true if on.""" From 1809b7a98ba038384d0d0d23cdfc4c5718e70764 Mon Sep 17 00:00:00 2001 From: Reuben Gow Date: Mon, 9 Aug 2021 19:47:38 +0100 Subject: [PATCH 146/199] Force an attempted subscribe on speaker reboot (#54100) * Force an attempted subscribe on speaker reboot * Recreate subscriptions and timers explicitly on speaker reboot * only create poll timer if there is not one already Co-authored-by: jjlawren * Black Co-authored-by: jjlawren --- homeassistant/components/sonos/speaker.py | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 434717f7a85..919e03cf39b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -496,9 +496,7 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_unseen( - self, now: datetime.datetime | None = None, will_reconnect: bool = False - ) -> None: + async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" if self._seen_timer: self._seen_timer() @@ -527,9 +525,8 @@ class SonosSpeaker: await self.async_unsubscribe() - if not will_reconnect: - self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) - self.async_write_entity_states() + self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) + self.async_write_entity_states() async def async_rebooted(self, soco: SoCo) -> None: """Handle a detected speaker reboot.""" @@ -538,8 +535,24 @@ class SonosSpeaker: self.zone_name, soco, ) - await self.async_unseen(will_reconnect=True) - await self.async_seen(soco) + await self.async_unsubscribe() + self.soco = soco + await self.async_subscribe() + if self._seen_timer: + self._seen_timer() + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + if not self._poll_timer: + self._poll_timer = self.hass.helpers.event.async_track_time_interval( + partial( + async_dispatcher_send, + self.hass, + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + ), + SCAN_INTERVAL, + ) + self.async_write_entity_states() # # Battery management From 746bb2997e0b941fd3469a0618e7fbf3cb3444d5 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 9 Aug 2021 21:11:53 +0200 Subject: [PATCH 147/199] Fix login to BMW services for rest_of_world and north_america (#54261) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 17aaa166942..8131ac1415c 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.16"], + "requirements": ["bimmer_connected==0.7.18"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 8c936677ba2..a5cadd9b259 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,7 +365,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.18 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9fbc43d789..ea3472684ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -220,7 +220,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.18 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 3edd505468cb1734bffcde7a96438bf867ce4416 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 14:13:55 -0500 Subject: [PATCH 148/199] Always set interfaces explicitly when IPv6 is present (#54268) --- homeassistant/components/zeroconf/__init__.py | 17 +++---- tests/components/zeroconf/test_init.py | 47 +++++++++++++++++-- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index cdb46318578..e7132f56b55 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -142,7 +142,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: zc_args: dict = {} adapters = await network.async_get_adapters(hass) - if _async_use_default_interface(adapters): + + ipv6 = True + if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): + ipv6 = False + zc_args["ip_version"] = IPVersion.V4Only + else: + zc_args["ip_version"] = IPVersion.All + + if not ipv6 and _async_use_default_interface(adapters): zc_args["interfaces"] = InterfaceChoice.Default else: interfaces = zc_args["interfaces"] = [] @@ -158,13 +166,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if adapter["ipv6"] and adapter["index"] not in interfaces: interfaces.append(adapter["index"]) - ipv6 = True - if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): - ipv6 = False - zc_args["ip_version"] = IPVersion.V4Only - else: - zc_args["ip_version"] = IPVersion.All - aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf_types, homekit_models = await asyncio.gather( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e1e346621fe..0db8f0f5227 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -794,11 +794,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero ), patch( "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, - ), patch( - "socket.if_nametoindex", - side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get( - iface, 0 - ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -827,3 +822,45 @@ async def test_get_announced_addresses(hass, mock_async_zeroconf): first_ip = ip_address("192.168.1.5").packed actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip) assert actual[0] == first_ip and set(actual) == expected + + +_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [ + { + "auto": True, + "default": True, + "enabled": True, + "index": 1, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [ + { + "address": "fe80::dead:beef:dead:beef", + "network_prefix": 64, + "flowinfo": 1, + "scope_id": 3, + } + ], + "name": "eth1", + } +] + + +async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf): + """Test interfaces are explicitly set when IPv6 is present.""" + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( + hass.config_entries.flow, "async_init" + ), patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, + ), patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zc.mock_calls[0] == call( + interfaces=["192.168.1.5", 1], ip_version=IPVersion.All + ) From 955a72080f7d92599bf550dd563fcda0f44ae2a3 Mon Sep 17 00:00:00 2001 From: ZeGuigui Date: Mon, 9 Aug 2021 11:38:16 +0200 Subject: [PATCH 149/199] Fix atom integration for long term statistics (#54285) * Fix atom integration for long term statistics * Remove commented code * Fix last_reset syntax * last_reset not an extra attribute * last_reset as utc * black formatting * isort fix --- homeassistant/components/atome/sensor.py | 40 +++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index bcb7b4f1ece..7295a9cee41 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -14,12 +14,13 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -87,12 +88,16 @@ class AtomeData: self._is_connected = None self._day_usage = None self._day_price = None + self._day_last_reset = None self._week_usage = None self._week_price = None + self._week_last_reset = None self._month_usage = None self._month_price = None + self._month_last_reset = None self._year_usage = None self._year_price = None + self._year_last_reset = None @property def live_power(self): @@ -137,6 +142,11 @@ class AtomeData: """Return latest daily usage value.""" return self._day_price + @property + def day_last_reset(self): + """Return latest daily last reset.""" + return self._day_last_reset + @Throttle(DAILY_SCAN_INTERVAL) def update_day_usage(self): """Return current daily power usage.""" @@ -144,6 +154,7 @@ class AtomeData: values = self.atome_client.get_consumption(DAILY_TYPE) self._day_usage = values["total"] / 1000 self._day_price = values["price"] + self._day_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) except KeyError as error: @@ -159,6 +170,11 @@ class AtomeData: """Return latest weekly usage value.""" return self._week_price + @property + def week_last_reset(self): + """Return latest weekly last reset value.""" + return self._week_last_reset + @Throttle(WEEKLY_SCAN_INTERVAL) def update_week_usage(self): """Return current weekly power usage.""" @@ -166,6 +182,7 @@ class AtomeData: values = self.atome_client.get_consumption(WEEKLY_TYPE) self._week_usage = values["total"] / 1000 self._week_price = values["price"] + self._week_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) except KeyError as error: @@ -181,6 +198,11 @@ class AtomeData: """Return latest monthly usage value.""" return self._month_price + @property + def month_last_reset(self): + """Return latest monthly last reset value.""" + return self._month_last_reset + @Throttle(MONTHLY_SCAN_INTERVAL) def update_month_usage(self): """Return current monthly power usage.""" @@ -188,6 +210,7 @@ class AtomeData: values = self.atome_client.get_consumption(MONTHLY_TYPE) self._month_usage = values["total"] / 1000 self._month_price = values["price"] + self._month_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) except KeyError as error: @@ -203,6 +226,11 @@ class AtomeData: """Return latest yearly usage value.""" return self._year_price + @property + def year_last_reset(self): + """Return latest yearly last reset value.""" + return self._year_last_reset + @Throttle(YEARLY_SCAN_INTERVAL) def update_year_usage(self): """Return current yearly power usage.""" @@ -210,6 +238,7 @@ class AtomeData: values = self.atome_client.get_consumption(YEARLY_TYPE) self._year_usage = values["total"] / 1000 self._year_price = values["price"] + self._year_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) except KeyError as error: @@ -219,19 +248,19 @@ class AtomeData: class AtomeSensor(SensorEntity): """Representation of a sensor entity for Atome.""" - _attr_device_class = DEVICE_CLASS_POWER - def __init__(self, data, name, sensor_type): """Initialize the sensor.""" self._attr_name = name self._data = data self._sensor_type = sensor_type + self._attr_state_class = STATE_CLASS_MEASUREMENT if sensor_type == LIVE_TYPE: + self._attr_device_class = DEVICE_CLASS_POWER self._attr_unit_of_measurement = POWER_WATT - self._attr_state_class = STATE_CLASS_MEASUREMENT else: + self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR def update(self): @@ -247,6 +276,9 @@ class AtomeSensor(SensorEntity): } else: self._attr_state = getattr(self._data, f"{self._sensor_type}_usage") + self._attr_last_reset = dt_util.as_utc( + getattr(self._data, f"{self._sensor_type}_last_reset") + ) self._attr_extra_state_attributes = { "price": getattr(self._data, f"{self._sensor_type}_price") } From cf92d45f072ce3c997528d7bac62ca5bca0ad0f0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 9 Aug 2021 14:55:58 -0400 Subject: [PATCH 150/199] Use correct state attribute for alarmdecoder binary sensor (#54286) Co-authored-by: Martin Hjelmare --- homeassistant/components/alarmdecoder/binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 397394e256b..430a4f73262 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -111,13 +111,13 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): def _fault_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: - self._attr_state = 1 + self._attr_is_on = True self.schedule_update_ha_state() def _restore_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or (int(zone) == self._zone_number and not self._loop): - self._attr_state = 0 + self._attr_is_on = False self.schedule_update_ha_state() def _rfx_message_callback(self, message): @@ -125,7 +125,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): if self._rfid and message and message.serial_number == self._rfid: rfstate = message.value if self._loop: - self._attr_state = 1 if message.loop[self._loop - 1] else 0 + self._attr_is_on = bool(message.loop[self._loop - 1]) attr = {CONF_ZONE_NUMBER: self._zone_number} if self._rfid and rfstate is not None: attr[ATTR_RF_BIT0] = bool(rfstate & 0x01) @@ -150,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): message.channel, message.value, ) - self._attr_state = message.value + self._attr_is_on = bool(message.value) self.schedule_update_ha_state() From 985dab6bdf6f56ca9730bcd7760c25ae60c39a4a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 9 Aug 2021 02:21:07 -0500 Subject: [PATCH 151/199] Bump soco to 0.23.3 (#54288) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 4ce5623ac38..d9c2a2cc6c9 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.23.2"], + "requirements": ["soco==0.23.3"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index a5cadd9b259..812bd5f3949 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2152,7 +2152,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.23.2 +soco==0.23.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea3472684ce..55fda7e459d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.23.2 +soco==0.23.3 # homeassistant.components.solaredge solaredge==0.0.2 From 5402173e98417825f4ee04e6eb9f7a73950c970d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 20:53:30 +0200 Subject: [PATCH 152/199] Fix ondilo_ico name attribute (#54290) --- homeassistant/components/ondilo_ico/sensor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 26a61ddfe4c..7449524d9e5 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -141,9 +141,9 @@ class OndiloICO(CoordinatorEntity, SensorEntity): self._poolid = self.coordinator.data[poolidx]["id"] pooldata = self._pooldata() - self._unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" + self._attr_unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" self._device_name = pooldata["name"] - self._name = f"{self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" def _pooldata(self): """Get pool data dict.""" @@ -168,11 +168,6 @@ class OndiloICO(CoordinatorEntity, SensorEntity): """Last value of the sensor.""" return self._devdata()["value"] - @property - def unique_id(self): - """Return the unique ID of this entity.""" - return self._unique_id - @property def device_info(self): """Return the device info for the sensor.""" From 9caad5b2c7fc99a693346c3c891da86acefb418a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 13:58:27 -0500 Subject: [PATCH 153/199] Bump zeroconf to 0.34.3 (#54294) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1847a1c806b..83db312601c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.4"], + "requirements": ["zeroconf==0.34.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6d22aa51b24..963cccf9ad2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.4 +zeroconf==0.34.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 812bd5f3949..2fb05e5348c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.4 +zeroconf==0.34.3 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55fda7e459d..26f69c82727 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.4 +zeroconf==0.34.3 # homeassistant.components.zha zha-quirks==0.0.59 From 2a1d2b77a120cc098f276cd38d526cf6a370b84a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 14:03:55 -0500 Subject: [PATCH 154/199] Ensure hunterdouglas_powerview model type is a string (#54299) --- homeassistant/components/hunterdouglas_powerview/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index bf0d5d564ff..db4b984703c 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -71,7 +71,7 @@ class ShadeEntity(HDEntity): "name": self._shade_name, "suggested_area": self._room_name, "manufacturer": MANUFACTURER, - "model": self._shade.raw_data[ATTR_TYPE], + "model": str(self._shade.raw_data[ATTR_TYPE]), "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), } From b21f319b0a0c832fb4ca7c0f8b7adc04acdcdf91 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 9 Aug 2021 04:21:41 -0700 Subject: [PATCH 155/199] Remove zwave_js transition on individual color channels (#54303) --- homeassistant/components/zwave_js/light.py | 2 +- tests/components/zwave_js/test_services.py | 9 +++++---- .../zwave_js/bulb_6_multi_color_state.json | 15 +++++---------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index f3cabe8b6a7..4f1de6c686d 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -301,7 +301,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # fallback to setting the color(s) one by one if multicolor fails # not sure this is needed at all, but just in case for color, value in colors.items(): - await self._async_set_color(color, value, zwave_transition) + await self._async_set_color(color, value) async def _async_set_color( self, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 3ee656e40c0..4cc5b599f19 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1021,8 +1021,7 @@ async def test_multicast_set_value_options( ], ATTR_COMMAND_CLASS: 51, ATTR_PROPERTY: "targetColor", - ATTR_PROPERTY_KEY: 2, - ATTR_VALUE: 2, + ATTR_VALUE: '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }', ATTR_OPTIONS: {"transitionDuration": 1}, }, blocking=True, @@ -1038,9 +1037,11 @@ async def test_multicast_set_value_options( assert args["valueId"] == { "commandClass": 51, "property": "targetColor", - "propertyKey": 2, } - assert args["value"] == 2 + assert ( + args["value"] + == '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }' + ) assert args["options"] == {"transitionDuration": 1} client.async_send_command.reset_mock() diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json index dfa72af6aa4..58608131e90 100644 --- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -267,8 +267,7 @@ "min": 0, "max": 255, "label": "Target value (Warm White)", - "description": "The target value of the Warm White color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Warm White color." } }, { @@ -286,8 +285,7 @@ "min": 0, "max": 255, "label": "Target value (Cold White)", - "description": "The target value of the Cold White color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Cold White color." } }, { @@ -305,8 +303,7 @@ "min": 0, "max": 255, "label": "Target value (Red)", - "description": "The target value of the Red color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Red color." } }, { @@ -324,8 +321,7 @@ "min": 0, "max": 255, "label": "Target value (Green)", - "description": "The target value of the Green color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Green color." } }, { @@ -343,8 +339,7 @@ "min": 0, "max": 255, "label": "Target value (Blue)", - "description": "The target value of the Blue color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Blue color." } }, { From 59c882a0f58a8a925c2857c8e53ee3258f838ec2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 9 Aug 2021 18:48:01 +0100 Subject: [PATCH 156/199] Restores unit_of_measurement (#54335) --- homeassistant/components/integration/sensor.py | 4 ++++ tests/components/integration/test_sensor.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index dea8970f4f7..b9fd1da4e42 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -145,6 +145,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + @callback def calc_integration(event): """Handle the sensor state changes.""" diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index dd6bf980d0f..36d3d4b3b30 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -73,6 +73,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: { "last_reset": "2019-10-06T21:00:00", "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, }, ), ), From cb14acd6066baac076e5b185684c296339902b67 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 9 Aug 2021 20:57:36 +0200 Subject: [PATCH 157/199] Fix xiaomi air fresh fan preset modes (#54342) --- homeassistant/components/xiaomi_miio/fan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c58d9ad0c66..05e32507b20 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1138,6 +1138,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): self._speed_list = OPERATION_MODES_AIRFRESH self._speed_count = 4 self._preset_modes = PRESET_MODES_AIRFRESH + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) From 84f65860581e6eb6d699225a68d5a692b191af60 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Aug 2021 22:27:09 +0200 Subject: [PATCH 158/199] Update frontend to 20210809.0 (#54350) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b9a84cbec02..135c0ec0244 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20210804.0"], + "requirements": [ + "home-assistant-frontend==20210809.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 963cccf9ad2..2e07d0adc18 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2fb05e5348c..fa23e5c3d53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26f69c82727..adf4c23fd03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3fcbde3b9cd829cdc710994f49d090588508352d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 9 Aug 2021 23:43:59 +0200 Subject: [PATCH 159/199] Fix Xiaomi-miio turn fan on with speed, percentage or preset (#54353) --- homeassistant/components/xiaomi_miio/fan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 05e32507b20..feeadf2bccc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -615,6 +615,10 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): **kwargs, ) -> None: """Turn the device on.""" + result = await self._try_command( + "Turning the miio device on failed.", self._device.on + ) + # Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented if speed: await self.async_set_speed(speed) @@ -623,10 +627,6 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): await self.async_set_percentage(percentage) if preset_mode: await self.async_set_preset_mode(preset_mode) - else: - result = await self._try_command( - "Turning the miio device on failed.", self._device.on - ) if result: self._state = True From cfa6040d55c8068f585078d46d8b60eb617a45e1 Mon Sep 17 00:00:00 2001 From: dailow Date: Mon, 9 Aug 2021 14:50:09 -0700 Subject: [PATCH 160/199] Fix aqualogic state attribute update (#54354) --- homeassistant/components/aqualogic/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 01f31757c9d..fff73cf00fa 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -93,9 +93,10 @@ class AquaLogicSensor(SensorEntity): if panel is not None: if panel.is_metric: self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0] - self._attr_state = getattr(panel, self._type) - self.async_write_ha_state() else: self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + + self._attr_state = getattr(panel, self._type) + self.async_write_ha_state() else: self._attr_unit_of_measurement = None From 4f3cf5a61c2cde0438db9894193710d01809d2ed Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 9 Aug 2021 14:50:39 -0700 Subject: [PATCH 161/199] Cast SimpliSafe version number as a string in device info (#54356) --- homeassistant/components/simplisafe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 0853aa3974c..924cf398f64 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -431,7 +431,7 @@ class SimpliSafeEntity(CoordinatorEntity): self._attr_device_info = { "identifiers": {(DOMAIN, system.system_id)}, "manufacturer": "SimpliSafe", - "model": system.version, + "model": str(system.version), "name": name, "via_device": (DOMAIN, system.serial), } From b4d466f87c1b9d9571c886dc87e84d8b33ee1d54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 16:45:56 -0700 Subject: [PATCH 162/199] Do not process forwarded for headers for cloud requests (#54364) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/http/forwarded.py | 11 ++++++++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/http/test_forwarded.py | 20 ++++++++++++++++++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 7516f32c3e1..abf73c1d54b 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.44.0"], + "requirements": ["hass-nabucasa==0.45.1"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 18bc51af1d1..9a76866ba21 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -63,12 +63,19 @@ def async_setup_forwarded( an HTTP 400 status code is thrown. """ + try: + from hass_nabucasa import remote # pylint: disable=import-outside-toplevel + except ImportError: + remote = None + @middleware async def forwarded_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """Process forwarded data by a reverse proxy.""" - overrides: dict[str, str] = {} + # Skip requests from Remote UI + if remote is not None and remote.is_cloud_request.get(): + return await handler(request) # Handle X-Forwarded-For forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, []) @@ -120,6 +127,8 @@ def async_setup_forwarded( ) raise HTTPBadRequest from err + overrides: dict[str, str] = {} + # Find the last trusted index in the X-Forwarded-For list forwarded_for_index = 0 for forwarded_ip in forwarded_for: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e07d0adc18..d03c4b7c4f7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index fa23e5c3d53..15858eee270 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adf4c23fd03..67b7b53458a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 # homeassistant.components.tasmota hatasmota==0.2.20 diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 400a1f32729..42e67416044 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -1,5 +1,6 @@ """Test real forwarded middleware.""" from ipaddress import ip_network +from unittest.mock import Mock, patch from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO @@ -441,3 +442,22 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): assert resp.status == 400 assert "Empty value received in X-Forward-Host header" in caplog.text + + +async def test_x_forwarded_cloud(aiohttp_client, caplog): + """Test that cloud requests are not processed.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""} + ) + + # This request would normally fail because it's invalid, now it works. + assert resp.status == 200 From d4290d1e031e67ebfc9761b5de7250cc226e1231 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 16:45:39 -0700 Subject: [PATCH 163/199] Revert "Use entity class attributes for Bluesound (#53033)" (#54365) --- .../components/bluesound/media_player.py | 185 +++++++++++------- 1 file changed, 115 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index a565a0f560c..86d0be72bdc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -203,29 +203,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC - - def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None): + def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" self.host = host self._hass = hass self.port = port self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. - self._attr_name = name + self._name = name + self._icon = None self._capture_items = [] self._services_items = [] self._preset_items = [] self._sync_status = {} self._status = None - self._is_online = None + self._last_status_update = None + self._is_online = False self._retry_remove = None + self._muted = False self._master = None - self._group_name = None - self._bluesound_device_name = None self._is_master = False + self._group_name = None self._group_list = [] + self._bluesound_device_name = None + self._init_callback = init_callback + if self.port is None: + self.port = DEFAULT_PORT class _TimeoutException(Exception): pass @@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity): return None self._sync_status = resp["SyncStatus"].copy() - if not self.name: - self._attr_name = self._sync_status.get("@name", self.host) + if not self._name: + self._name = self._sync_status.get("@name", self.host) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) - if not self.icon: - self._attr_icon = self._sync_status.get("@icon", self.host) + if not self._icon: + self._icon = self._sync_status.get("@icon", self.host) master = self._sync_status.get("master") if master is not None: @@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): - _LOGGER.info("Node %s is offline, retrying later", self.name) + _LOGGER.info("Node %s is offline, retrying later", self._name) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s", self.name) + _LOGGER.debug("Stopping the polling of node %s", self._name) except Exception: - _LOGGER.exception("Unexpected error in %s", self.name) + _LOGGER.exception("Unexpected error in %s", self._name) raise def start_polling(self): @@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity): if response.status == HTTP_OK: result = await response.text() self._is_online = True - self._attr_media_position_updated_at = dt_util.utcnow() + self._last_status_update = dt_util.utcnow() self._status = xmltodict.parse(result)["status"].copy() group_name = self._status.get("groupName") @@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, ClientError): self._is_online = False - self._attr_media_position_updated_at = None + self._last_status_update = None self._status = None self.async_write_ha_state() - _LOGGER.info("Client connection error, marking %s as offline", self.name) + _LOGGER.info("Client connection error, marking %s as offline", self._name) raise - self.update_state_attr() - - def update_state_attr(self): - """Update state attributes.""" - if self._status is None: - self._attr_state = STATE_OFF - self._attr_supported_features = 0 - elif self.is_grouped and not self.is_master: - self._attr_state = STATE_GROUPED - self._attr_supported_features = ( - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE - ) - else: - status = self._status.get("state") - self._attr_state = STATE_IDLE - if status in ("pause", "stop"): - self._attr_state = STATE_PAUSED - elif status in ("stream", "play"): - self._attr_state = STATE_PLAYING - supported = SUPPORT_CLEAR_PLAYLIST - if self._status.get("indexing", "0") == "0": - supported = ( - supported - | SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_PLAY - | SUPPORT_SELECT_SOURCE - | SUPPORT_SHUFFLE_SET - ) - if self.volume_level is not None and self.volume_level >= 0: - supported = ( - supported - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - ) - if self._status.get("canSeek", "") == "1": - supported = supported | SUPPORT_SEEK - self._attr_supported_features = supported - self._attr_extra_state_attributes = {} - if self._group_list: - self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list} - self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master - self._attr_shuffle = self._status.get("shuffle", "0") == "1" async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" @@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def state(self): + """Return the state of the device.""" + if self._status is None: + return STATE_OFF + + if self.is_grouped and not self.is_master: + return STATE_GROUPED + + status = self._status.get("state") + if status in ("pause", "stop"): + return STATE_PAUSED + if status in ("stream", "play"): + return STATE_PLAYING + return STATE_IDLE + @property def media_title(self): """Title of current playing media.""" @@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity): return None mediastate = self.state - if self.media_position_updated_at is None or mediastate == STATE_IDLE: + if self._last_status_update is None or mediastate == STATE_IDLE: return None position = self._status.get("secs") @@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity): position = float(position) if mediastate == STATE_PLAYING: - position += ( - dt_util.utcnow() - self.media_position_updated_at - ).total_seconds() + position += (dt_util.utcnow() - self._last_status_update).total_seconds() return position @@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity): return None return float(duration) + @property + def media_position_updated_at(self): + """Last time status was updated.""" + return self._last_status_update + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute + @property + def name(self): + """Return the name of the device.""" + return self._name + @property def bluesound_device_name(self): """Return the device name as returned by the device.""" return self._bluesound_device_name + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + @property def source_list(self): """List of available input sources.""" @@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity): return None @property - def is_master(self) -> bool: + def supported_features(self): + """Flag of media commands that are supported.""" + if self._status is None: + return 0 + + if self.is_grouped and not self.is_master: + return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE + + supported = SUPPORT_CLEAR_PLAYLIST + + if self._status.get("indexing", "0") == "0": + supported = ( + supported + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + ) + + current_vol = self.volume_level + if current_vol is not None and current_vol >= 0: + supported = ( + supported + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + ) + + if self._status.get("canSeek", "") == "1": + supported = supported | SUPPORT_SEEK + + return supported + + @property + def is_master(self): """Return true if player is a coordinator.""" return self._is_master @property - def is_grouped(self) -> bool: + def is_grouped(self): """Return true if player is a coordinator.""" return self._master is not None or self._is_master + @property + def shuffle(self): + """Return true if shuffle is active.""" + return self._status.get("shuffle", "0") == "1" + async def async_join(self, master): """Join the player to a group.""" master_device = [ @@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity): else: _LOGGER.error("Master not found %s", master_device) + @property + def extra_state_attributes(self): + """List members in group.""" + attributes = {} + if self._group_list: + attributes = {ATTR_BLUESOUND_GROUP: self._group_list} + + attributes[ATTR_MASTER] = self._is_master + + return attributes + def rebuild_bluesound_group(self): """Rebuild the list of entities in speaker group.""" if self._group_name is None: From 0b532c139c70cbb9c0c92f932b19ab1968fb0aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 17:14:06 -0700 Subject: [PATCH 164/199] Bumped version to 2021.8.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4c7ab9742b0..803ab0751e6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 5ac5b41a11517b2fee3ecdd564d26137c328a1a2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:16:18 -0400 Subject: [PATCH 165/199] Update Climacell rate limit (#54373) --- homeassistant/components/climacell/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 9e80c769abf..162fbb01545 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -66,7 +66,7 @@ DEFAULT_FORECAST_TYPE = DAILY DOMAIN = "climacell" ATTRIBUTION = "Powered by ClimaCell" -MAX_REQUESTS_PER_DAY = 500 +MAX_REQUESTS_PER_DAY = 100 CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} From 747eb92a4a3b1ab32285d227c9da8484c3f4d135 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 10 Aug 2021 13:21:24 +1000 Subject: [PATCH 166/199] Fix race condition in Advantage Air (#53439) --- .../components/advantage_air/climate.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1d377abc065..1e6027b8db6 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.core import callback from homeassistant.helpers import entity_platform from .const import ( @@ -166,19 +165,22 @@ class AdvantageAirZone(AdvantageAirClimateEntity): f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' ) - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) - - @callback - def _update_callback(self) -> None: - """Load data from integration.""" - self._attr_current_temperature = self._zone["measuredTemp"] - self._attr_target_temperature = self._zone["setTemp"] - self._attr_hvac_mode = HVAC_MODE_OFF + @property + def hvac_mode(self): + """Return the current state as HVAC mode.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: - self._attr_hvac_mode = HVAC_MODE_FAN_ONLY - self.async_write_ha_state() + return HVAC_MODE_FAN_ONLY + return HVAC_MODE_OFF + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._zone["measuredTemp"] + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._zone["setTemp"] async def async_set_hvac_mode(self, hvac_mode): """Set the HVAC Mode and State.""" From 3effe94a2702db4422a557399e9c67b193d9a3ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 23:17:47 -0700 Subject: [PATCH 167/199] Handle CO2Signal response value being None (#54377) --- homeassistant/components/co2signal/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index bd8d94355fd..ea1cd1f6169 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -118,7 +118,8 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE def available(self) -> bool: """Return True if entity is available.""" return ( - super().available and self._description.key in self.coordinator.data["data"] + super().available + and self.coordinator.data["data"].get(self._description.key) is not None ) @property From bb3769c84f92137e5d6fb71735abe4f5433717b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 09:19:28 +0200 Subject: [PATCH 168/199] Fix Canary sensor state (#54380) --- homeassistant/components/canary/sensor.py | 6 +++++- tests/components/canary/test_sensor.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 5c92f0089f2..acb885055a3 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -113,7 +113,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity): canary_sensor_type = SensorType.BATTERY self._canary_type = canary_sensor_type - self._attr_state = self.reading self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}" self._attr_device_info = { "identifiers": {(DOMAIN, str(device.device_id))}, @@ -144,6 +143,11 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + return self.reading + @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes.""" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 6419f81a62e..67d4a724584 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -118,9 +118,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.async_block_till_done() entity_id = "sensor.home_dining_room_air_quality" - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL + state1 = hass.states.get(entity_id) + assert state1 + assert state1.state == "0.59" + assert state1.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL instance.get_latest_readings.return_value = [ mock_reading("temperature", "21.12"), @@ -133,9 +134,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL + state2 = hass.states.get(entity_id) + assert state2 + assert state2.state == "0.4" + assert state2.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL instance.get_latest_readings.return_value = [ mock_reading("temperature", "21.12"), @@ -148,9 +150,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL + state3 = hass.states.get(entity_id) + assert state3 + assert state3.state == "1.0" + assert state3.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL async def test_sensors_flex(hass, canary) -> None: From 56d51404b7b88c266e99abfd4424c24935f076e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 10 Aug 2021 20:14:10 +0200 Subject: [PATCH 169/199] Re-add Tibber notify service name (#54401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index a18bb855f8f..da94df55c88 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -5,7 +5,7 @@ import logging import aiohttp import tibber -from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry): # have to use discovery to load platform. hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG] + hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG] ) ) return True From ebab5c5d9240e593dc3743a25c372f70e3ea98f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Aug 2021 11:30:02 -0700 Subject: [PATCH 170/199] Bump hass_nabucasa to 0.46.0 (#54421) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index abf73c1d54b..129b9f83819 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.45.1"], + "requirements": ["hass-nabucasa==0.46.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d03c4b7c4f7..323b1c86034 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 15858eee270..f151f2a5048 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67b7b53458a..35bdaf3d908 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 # homeassistant.components.tasmota hatasmota==0.2.20 From b2b0cc9a22a648e30668db01eecd45047f51da53 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 10 Aug 2021 20:28:03 -0700 Subject: [PATCH 171/199] Bump pyopenuv to 2.1.0 (#54436) --- homeassistant/components/openuv/__init__.py | 2 +- homeassistant/components/openuv/config_flow.py | 2 +- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index efe6fa89ca8..bcdd0b2ba40 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -59,8 +59,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_API_KEY], config_entry.data.get(CONF_LATITUDE, hass.config.latitude), config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - websession, altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + session=websession, ) ) await openuv.async_update() diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 54b2aca0b75..3595b124053 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -71,7 +71,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(user_input[CONF_API_KEY], 0, 0, websession) + client = Client(user_input[CONF_API_KEY], 0, 0, session=websession) try: await client.uv_index() diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 81e38d251f1..842d4966805 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==1.0.9"], + "requirements": ["pyopenuv==2.1.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f151f2a5048..82edfd81d57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1649,7 +1649,7 @@ pyobihai==1.3.1 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==1.0.9 +pyopenuv==2.1.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35bdaf3d908..8245b165cff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.openuv -pyopenuv==1.0.9 +pyopenuv==2.1.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From c6fc5e35fe432f7b42d315296a1844793d194169 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Aug 2021 20:29:32 -0700 Subject: [PATCH 172/199] Bumped version to 2021.8.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 803ab0751e6..8765dcbc94b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 5fc5b53c01574874b711e757c4512e73cba5b1bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 20:28:01 -0500 Subject: [PATCH 173/199] Ensure HomeKit passes min/max mireds as ints - adapted from #54372 --- homeassistant/components/homekit/type_lights.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 88e21272a4f..2a1256dfddb 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,5 +1,6 @@ """Class to hold all light accessories.""" import logging +import math from pyhap.const import CATEGORY_LIGHTBULB @@ -115,8 +116,8 @@ class Light(HomeAccessory): ) if self.is_color_temp_supported: - min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) - max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) + min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) + max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) serv_light = serv_light_secondary or serv_light_primary self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, From 7d67caba44ed0f472fa36d7793385d2724eb7748 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 11 Aug 2021 15:34:36 +0100 Subject: [PATCH 174/199] Use pycarwings2 2.11 (#54424) --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 298343d2d8d..55cd28d59fa 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,7 +2,7 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.10"], + "requirements": ["pycarwings2==2.11"], "codeowners": ["@filcole"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 82edfd81d57..8d3537194e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1342,7 +1342,7 @@ pyblackbird==0.5 pybotvac==0.0.21 # homeassistant.components.nissan_leaf -pycarwings2==2.10 +pycarwings2==2.11 # homeassistant.components.cloudflare pycfdns==1.2.1 From 72e548de5f140de595157f679878b502bfbba699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 11 Aug 2021 12:50:17 +0300 Subject: [PATCH 175/199] Fix Huawei LTE entity state updating (#54447) Since 91a2b96, we no longer key this by the router URL, but the relevant config entry unique id. Closes https://github.com/home-assistant/core/issues/54243 --- homeassistant/components/huawei_lte/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 81b715b71fe..0c545486c82 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -665,9 +665,9 @@ class HuaweiLteBaseEntity(Entity): async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) ) - async def _async_maybe_update(self, url: str) -> None: + async def _async_maybe_update(self, config_entry_unique_id: str) -> None: """Update state if the update signal comes from our router.""" - if url == self.router.url: + if config_entry_unique_id == self.router.config_entry.unique_id: self.async_schedule_update_ha_state(True) async def async_will_remove_from_hass(self) -> None: From 4d01dd3c0cf5c880f6968e9dbea214df9e18335f Mon Sep 17 00:00:00 2001 From: Felix <0xFelix@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:17:12 +0200 Subject: [PATCH 176/199] Strip attributes whitespace in universal media_player (#54451) When using whitespace in attributes the lookup of the state attribute fails because an entity with whitespace in its name cannot be found. Works: entity_id|state_attribute Does not work: entity_id | state_attribute Fixes #53804 --- homeassistant/components/universal/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index a2981852dc1..f3d4a6f1355 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -158,7 +158,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._cmds = commands self._attrs = {} for key, val in attributes.items(): - attr = val.split("|", 1) + attr = list(map(str.strip, val.split("|", 1))) if len(attr) == 1: attr.append(None) self._attrs[key] = attr From 1d45a80a92b9b982693b8793594fc3a2f7267869 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 11 Aug 2021 10:13:38 -0400 Subject: [PATCH 177/199] Bump notifications-android-tv to 0.1.3 (#54462) Co-authored-by: Martin Hjelmare --- homeassistant/components/nfandroidtv/__init__.py | 7 +------ homeassistant/components/nfandroidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 90a76c1c747..35aecdb6916 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -1,6 +1,4 @@ """The NFAndroidTV integration.""" -import logging - from notifications_android_tv.notifications import ConnectError, Notifications from homeassistant.components.notify import DOMAIN as NOTIFY @@ -12,8 +10,6 @@ from homeassistant.helpers import discovery from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - PLATFORMS = [NOTIFY] @@ -41,8 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(Notifications, host) except ConnectError as ex: - _LOGGER.warning("Failed to connect: %s", ex) - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady("Failed to connect") from ex hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 5516f144fd4..c1dea03aa09 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,7 +2,7 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / Fire TV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "requirements": ["notifications-android-tv==0.1.2"], + "requirements": ["notifications-android-tv==0.1.3"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 8d3537194e1..2c79bc76a69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ niluclient==0.1.2 noaa-coops==0.1.8 # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.2 +notifications-android-tv==0.1.3 # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8245b165cff..b8f2683ee3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -579,7 +579,7 @@ nettigo-air-monitor==1.0.0 nexia==0.9.11 # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.2 +notifications-android-tv==0.1.3 # homeassistant.components.notify_events notify-events==1.0.4 From 6382061b5791e7a148d4cf84a4085d43a91f1d96 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 11 Aug 2021 21:17:25 -0600 Subject: [PATCH 178/199] Updates to bump MyQ to 3.1.2 (#54488) --- .coveragerc | 1 + homeassistant/components/myq/config_flow.py | 2 +- homeassistant/components/myq/cover.py | 8 ++++++-- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 1f28a9a2aee..6ad8658c702 100644 --- a/.coveragerc +++ b/.coveragerc @@ -672,6 +672,7 @@ omit = homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py homeassistant/components/myq/__init__.py + homeassistant/components/myq/cover.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 78a751a18b1..8c088de6715 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -31,7 +31,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate the user input allows us to connect.""" websession = aiohttp_client.async_get_clientsession(self.hass) try: - await pymyq.login(username, password, websession) + await pymyq.login(username, password, websession, True) except InvalidCredentialsError: return {CONF_PASSWORD: "invalid_auth"} except MyQError: diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 3d587635f2d..8d36db8e0ab 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -115,7 +115,9 @@ class MyQDevice(CoordinatorEntity, CoverEntity): # Write closing state to HASS self.async_write_ha_state() - if not await wait_task: + result = wait_task if isinstance(wait_task, bool) else await wait_task + + if not result: _LOGGER.error("Closing of cover %s failed", self._device.name) # Write final state to HASS @@ -137,7 +139,9 @@ class MyQDevice(CoordinatorEntity, CoverEntity): # Write opening state to HASS self.async_write_ha_state() - if not await wait_task: + result = wait_task if isinstance(wait_task, bool) else await wait_task + + if not result: _LOGGER.error("Opening of cover %s failed", self._device.name) # Write final state to HASS diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index a93501c941f..a4de12290f1 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==3.0.4"], + "requirements": ["pymyq==3.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 2c79bc76a69..925fcd9975b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1610,7 +1610,7 @@ pymonoprice==0.3 pymsteams==0.1.12 # homeassistant.components.myq -pymyq==3.0.4 +pymyq==3.1.2 # homeassistant.components.mysensors pymysensors==0.21.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8f2683ee3a..1b9a882f8f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -917,7 +917,7 @@ pymodbus==2.5.2 pymonoprice==0.3 # homeassistant.components.myq -pymyq==3.0.4 +pymyq==3.1.2 # homeassistant.components.mysensors pymysensors==0.21.0 From 6f3879fc92ac17efa845c99d3ba88654157d288b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Aug 2021 07:30:35 +0200 Subject: [PATCH 179/199] Add missing PRESSURE_BAR conversion (#54497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing PRESSURE_BAR * style Signed-off-by: Daniel Hjelseth Høyer * valid units Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/util/pressure.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 188cf66491e..95b32a69643 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -5,6 +5,7 @@ from numbers import Number from homeassistant.const import ( PRESSURE, + PRESSURE_BAR, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_MBAR, @@ -16,6 +17,7 @@ from homeassistant.const import ( VALID_UNITS: tuple[str, ...] = ( PRESSURE_PA, PRESSURE_HPA, + PRESSURE_BAR, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_PSI, @@ -24,6 +26,7 @@ VALID_UNITS: tuple[str, ...] = ( UNIT_CONVERSION: dict[str, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, + PRESSURE_BAR: 1 / 100000, PRESSURE_MBAR: 1 / 100, PRESSURE_INHG: 1 / 3386.389, PRESSURE_PSI: 1 / 6894.757, From 482e00a07145a7cbb1409830c96bcbdcda7ae4bc Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 15 Aug 2021 06:09:06 -0700 Subject: [PATCH 180/199] Treat temporary errors as warnings for Tesla (#54515) * Treat temporary errors as warnings for Tesla closes #53391 * Apply suggestions from code review Co-authored-by: J. Nick Koston * Black Co-authored-by: J. Nick Koston --- homeassistant/components/tesla/__init__.py | 9 +++++++++ homeassistant/components/tesla/config_flow.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index d945d87243e..798e769dc47 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -177,6 +177,15 @@ async def async_setup_entry(hass, config_entry): await async_client.aclose() if ex.code == HTTP_UNAUTHORIZED: raise ConfigEntryAuthFailed from ex + if ex.message in [ + "VEHICLE_UNAVAILABLE", + "TOO_MANY_REQUESTS", + "SERVICE_MAINTENANCE", + "UPSTREAM_TIMEOUT", + ]: + raise ConfigEntryNotReady( + f"Temporarily unable to communicate with Tesla API: {ex.message}" + ) from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 46bc49b126b..5a88999a7e3 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -175,7 +175,7 @@ async def validate_input(hass: core.HomeAssistant, data): if ex.code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials: %s", ex) raise InvalidAuth() from ex - _LOGGER.error("Unable to communicate with Tesla API: %s", ex) + _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) raise CannotConnect() from ex finally: await async_client.aclose() From 1c503244c6f8fbabc98d0d36b5720e6414292fcf Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 12 Aug 2021 23:33:02 +0200 Subject: [PATCH 181/199] Fix attributes not showing after using entity class attributes (#54558) --- .../bmw_connected_drive/__init__.py | 5 -- .../bmw_connected_drive/binary_sensor.py | 51 ++++++++----------- .../bmw_connected_drive/device_tracker.py | 1 + 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 3bd2365f88e..17e57b5d09c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -335,11 +335,6 @@ class BMWConnectedDriveBaseEntity(Entity): "manufacturer": vehicle.attributes.get("brand"), } - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return self._attrs - def update_callback(self): """Schedule a state update.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d7f0d150193..a7fd72fc1a7 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -85,54 +85,38 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): def update(self): """Read new state data from the library.""" vehicle_state = self._vehicle.state + result = self._attrs.copy() # device class opening: On means open, Off means closed if self._attribute == "lids": _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._attr_state = not vehicle_state.all_lids_closed - if self._attribute == "windows": - self._attr_state = not vehicle_state.all_windows_closed - # device class lock: On means unlocked, Off means locked - if self._attribute == "door_lock_state": - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._attr_state = vehicle_state.door_lock_state not in [ - LockState.LOCKED, - LockState.SECURED, - ] - # device class light: On means light detected, Off means no light - if self._attribute == "lights_parking": - self._attr_state = vehicle_state.are_parking_lights_on - # device class problem: On means problem detected, Off means no problem - if self._attribute == "condition_based_services": - self._attr_state = not vehicle_state.are_all_cbs_ok - if self._attribute == "check_control_messages": - self._attr_state = vehicle_state.has_check_control_messages - # device class power: On means power detected, Off means no power - if self._attribute == "charging_status": - self._attr_state = vehicle_state.charging_status in [ChargingState.CHARGING] - # device class plug: On means device is plugged in, - # Off means device is unplugged - if self._attribute == "connection_status": - self._attr_state = vehicle_state.connection_status == "CONNECTED" - - vehicle_state = self._vehicle.state - result = self._attrs.copy() - - if self._attribute == "lids": + self._attr_is_on = not vehicle_state.all_lids_closed for lid in vehicle_state.lids: result[lid.name] = lid.state.value elif self._attribute == "windows": + self._attr_is_on = not vehicle_state.all_windows_closed for window in vehicle_state.windows: result[window.name] = window.state.value + # device class lock: On means unlocked, Off means locked elif self._attribute == "door_lock_state": + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._attr_is_on = vehicle_state.door_lock_state not in [ + LockState.LOCKED, + LockState.SECURED, + ] result["door_lock_state"] = vehicle_state.door_lock_state.value result["last_update_reason"] = vehicle_state.last_update_reason + # device class light: On means light detected, Off means no light elif self._attribute == "lights_parking": + self._attr_is_on = vehicle_state.are_parking_lights_on result["lights_parking"] = vehicle_state.parking_lights.value + # device class problem: On means problem detected, Off means no problem elif self._attribute == "condition_based_services": + self._attr_is_on = not vehicle_state.are_all_cbs_ok for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": + self._attr_is_on = vehicle_state.has_check_control_messages check_control_messages = vehicle_state.check_control_messages has_check_control_messages = vehicle_state.has_check_control_messages if has_check_control_messages: @@ -142,13 +126,18 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" + # device class power: On means power detected, Off means no power elif self._attribute == "charging_status": + self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING] result["charging_status"] = vehicle_state.charging_status.value result["last_charging_end_result"] = vehicle_state.last_charging_end_result + # device class plug: On means device is plugged in, + # Off means device is unplugged elif self._attribute == "connection_status": + self._attr_is_on = vehicle_state.connection_status == "CONNECTED" result["connection_status"] = vehicle_state.connection_status - self._attr_extra_state_attributes = sorted(result.items()) + self._attr_extra_state_attributes = result def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 62b2ed9b9d9..c788051dc9a 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -59,6 +59,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def update(self): """Update state of the decvice tracker.""" + self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.state.gps_position if self._vehicle.state.is_vehicle_tracking_enabled From ab823693209614503e577e20d44b93ada98c1c63 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 14 Aug 2021 02:44:52 -0400 Subject: [PATCH 182/199] Upgrade qnapstats library to 0.4.0 (#54571) --- homeassistant/components/qnap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index abd5d6f5a4a..217d14a6adf 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -2,7 +2,7 @@ "domain": "qnap", "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", - "requirements": ["qnapstats==0.3.1"], + "requirements": ["qnapstats==0.4.0"], "codeowners": ["@colinodell"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 925fcd9975b..3906e42902e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qnap -qnapstats==0.3.1 +qnapstats==0.4.0 # homeassistant.components.quantum_gateway quantum-gateway==0.0.5 From f43151081bbc6e053063788747956e86ead74ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 13 Aug 2021 11:38:14 +0200 Subject: [PATCH 183/199] Fix bug in ambiclimate (#54579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix ambiclimate Signed-off-by: Daniel Hjelseth Høyer * Fix ambiclimate Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/ambiclimate/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 8cfebb1bf69..aa4be202865 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -154,8 +154,6 @@ class AmbiclimateEntity(ClimateEntity): "name": self.name, "manufacturer": "Ambiclimate", } - self._attr_min_temp = heater.get_min_temp() - self._attr_max_temp = heater.get_max_temp() async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -184,6 +182,8 @@ class AmbiclimateEntity(ClimateEntity): await self._store.async_save(token_info) data = await self._heater.update_device() + self._attr_min_temp = self._heater.get_min_temp() + self._attr_max_temp = self._heater.get_max_temp() self._attr_target_temperature = data.get("target_temperature") self._attr_current_temperature = data.get("temperature") self._attr_current_humidity = data.get("humidity") From 0f60a5a8c0b430d189c0db53e4a53c9b198e037e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Aug 2021 04:57:18 +0200 Subject: [PATCH 184/199] Fix Tibber last reset (#54582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index b5012cdc41d..f092e1d8f55 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -137,6 +137,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.NEVER, ), "lastMeterProduction": TibberSensorEntityDescription( key="lastMeterProduction", @@ -144,6 +145,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.NEVER, ), "voltagePhase1": TibberSensorEntityDescription( key="voltagePhase1", From 5a5bbef1b8b3ccf4f10f1903d1243bb1cc4b7b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 15 Aug 2021 13:16:10 +0200 Subject: [PATCH 185/199] Adax, update requirements (#54587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 36106290ed6..3d2c9273d05 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", "requirements": [ - "adax==0.0.1" + "adax==0.1.1" ], "codeowners": [ "@danielhiversen" diff --git a/requirements_all.txt b/requirements_all.txt index 3906e42902e..3bb90bec9cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,7 +106,7 @@ adafruit-circuitpython-dht==3.6.0 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.adax -adax==0.0.1 +adax==0.1.1 # homeassistant.components.androidtv adb-shell[async]==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b9a882f8f3..ff4b2e61468 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ abodepy==1.2.0 accuweather==0.2.0 # homeassistant.components.adax -adax==0.0.1 +adax==0.1.1 # homeassistant.components.androidtv adb-shell[async]==0.3.4 From 2f2038c14723f28b55c6b9eec1a63bbd4f605d78 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sat, 14 Aug 2021 08:27:47 +0200 Subject: [PATCH 186/199] Clamp color temperature to supported range in ESPHome light (#54595) ESPHome devices initially report a color temperature of 0 or 1 until it has been changed by the user. This broke the conversion from RGBWW to an RGB color. Fixes #54293. --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index b89a75ab76a..aeecc22d9f1 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -230,7 +230,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # Try to reverse white + color temp to cwww min_ct = self._static_info.min_mireds max_ct = self._static_info.max_mireds - color_temp = self._state.color_temperature + color_temp = min(max(self._state.color_temperature, min_ct), max_ct) white = self._state.white ww_frac = (color_temp - min_ct) / (max_ct - min_ct) From 3c3a6e6cb4959e34bf012aaef6f02ba726d52c40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Aug 2021 01:26:57 -0500 Subject: [PATCH 187/199] Bump zeroconf to 0.35.0 (#54604) Fixes https://github.com/home-assistant/core/issues/54531 Fixes https://github.com/home-assistant/core/issues/54434 Fixes https://github.com/home-assistant/core/issues/54487 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 83db312601c..b971ec06179 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.34.3"], + "requirements": ["zeroconf==0.35.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 323b1c86034..1b0d7a3e12d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.34.3 +zeroconf==0.35.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 3bb90bec9cf..54c982abf07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.34.3 +zeroconf==0.35.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff4b2e61468..b3a53cf7102 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.34.3 +zeroconf==0.35.0 # homeassistant.components.zha zha-quirks==0.0.59 From 8d1bd55b68d80216e534b8480e801bf7a3ef8a67 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Aug 2021 21:33:48 +0200 Subject: [PATCH 188/199] Bump py-synologydsm-api to 1.0.4 (#54610) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 04d7f43bb75..8d8d30c2cf8 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.3"], + "requirements": ["py-synologydsm-api==1.0.4"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 54c982abf07..2d026810c3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1254,7 +1254,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.3 +py-synologydsm-api==1.0.4 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3a53cf7102..f1619c2319c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.3 +py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack py17track==3.2.1 From dd1ef7fa5553f0a5633d24fdd33236bd23168af0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Aug 2021 19:56:56 -0700 Subject: [PATCH 189/199] Guard partial upgrade (#54617) --- homeassistant/components/http/forwarded.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 9a76866ba21..6dd2d9adb8a 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -65,6 +65,11 @@ def async_setup_forwarded( try: from hass_nabucasa import remote # pylint: disable=import-outside-toplevel + + # venv users might have already loaded it before it got upgraded so guard for this + # This can only happen when people upgrade from before 2021.8.5. + if not hasattr(remote, "is_cloud_request"): + remote = None except ImportError: remote = None From 5af94c42dbfe4822b16fc86fd4f03d0261163a98 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Aug 2021 04:57:37 +0200 Subject: [PATCH 190/199] Solve switch/verify register type convert problem in modbus (#54645) --- homeassistant/components/modbus/base_platform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 5b3cdfb48ee..288cfc7022a 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -189,9 +189,9 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._verify_address = config[CONF_VERIFY].get( CONF_ADDRESS, config[CONF_ADDRESS] ) - self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, convert[config[CONF_WRITE_TYPE]][0] - ) + self._verify_type = convert[ + config[CONF_VERIFY].get(CONF_INPUT_TYPE, config[CONF_WRITE_TYPE]) + ][0] self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self.command_on) self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) else: From 848885c658800905950c3b7c86cae089e5b0f494 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Aug 2021 14:56:21 +1200 Subject: [PATCH 191/199] Send color_brightness to ESPHome devices on 1.20 (pre-color_mode) (#54670) --- homeassistant/components/esphome/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index aeecc22d9f1..c6cf9742082 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -105,8 +105,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): color_bri = max(rgb) # normalize rgb data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = LightColorMode.RGB if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: @@ -116,8 +116,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # normalize rgb data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) data["white"] = w + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = LightColorMode.RGB_WHITE if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: @@ -144,8 +144,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): data["color_temperature"] = min_ct + ct_ratio * (max_ct - min_ct) target_mode = LightColorMode.RGB_COLOR_TEMPERATURE + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = target_mode if (flash := kwargs.get(ATTR_FLASH)) is not None: From 700f149ef86f1054522706b3899a760490abc5a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Aug 2021 20:06:46 -0700 Subject: [PATCH 192/199] Bumped version to 2021.8.7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8765dcbc94b..c96f621ade2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From d0b1caa8b067d511d053c6110341d4a1d8aaf5d7 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Wed, 18 Aug 2021 20:51:48 +0100 Subject: [PATCH 193/199] Fix TPLink emeter reset not updating (#54848) --- homeassistant/components/tplink/__init__.py | 18 +++++++++++++++--- homeassistant/components/tplink/common.py | 14 ++++++++++++++ homeassistant/components/tplink/sensor.py | 8 ++++++++ tests/components/tplink/test_init.py | 13 ++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 552e5666db8..565876b1c7c 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -28,9 +28,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.dt import as_local, utc_from_timestamp -from .common import SmartDevices, async_discover_devices, get_static_devices +from .common import ( + SmartDevices, + async_discover_devices, + get_static_devices, + get_time_offset, +) from .const import ( ATTR_CONFIG, ATTR_CURRENT_A, @@ -261,9 +266,16 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, } emeter_statics = self.smartplug.get_emeter_daily() + last_reset = datetime.now() - get_time_offset(self.smartplug) + last_reset_local = as_local(last_reset.replace(second=0, microsecond=0)) + _LOGGER.debug( + "%s last reset time as local to server is %s", + self.smartplug.alias, + last_reset_local.strftime("%Y/%m/%d %H:%M:%S"), + ) data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ ATTR_TODAY_ENERGY_KWH - ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + ] = last_reset_local if emeter_statics.get(int(time.strftime("%e"))): data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( float(emeter_statics[int(time.strftime("%e"))]), 3 diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 6f6fb0a14c2..8acd6f29cba 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -1,6 +1,7 @@ """Common code for tplink.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Callable @@ -184,3 +185,16 @@ def add_available_devices( hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"] = devices_unavailable return entities_ready + + +def get_time_offset(device: SmartDevice) -> timedelta: + """Get the time offset since last device reset (local midnight).""" + device_time = device.time.replace(microsecond=0) + offset = device_time - device_time.replace(hour=0, minute=0, second=0) + _LOGGER.debug( + "%s local time is %s, offset from midnight is %s", + device.alias, + device_time.strftime("%Y/%m/%d %H:%M:%S"), + str(offset), + ) + return offset diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 697641915f7..3d7f26bb786 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,6 +1,7 @@ """Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" from __future__ import annotations +from datetime import datetime from typing import Any, Final from pyHS100 import SmartPlug @@ -156,3 +157,10 @@ class SmartPlugSensor(CoordinatorEntity, SensorEntity): "connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])}, "sw_version": self.data[CONF_SW_VERSION], } + + @property + def last_reset(self) -> datetime | None: + """Return the last reset time for emeter.""" + return self.data[CONF_EMETER_PARAMS][ATTR_LAST_RESET].get( + self.entity_description.key + ) diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index d96d6846939..6139fae9b11 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,6 +1,7 @@ """Tests for the TP-Link component.""" from __future__ import annotations +from datetime import datetime import time from typing import Any from unittest.mock import MagicMock, patch @@ -222,6 +223,11 @@ async def test_platforms_are_initialized(hass: HomeAssistant): ), patch( "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False, + ), patch( + "homeassistant.components.tplink.get_time_offset", + return_value=( + datetime.now() - datetime.now().replace(hour=0, minute=0, second=0) + ), ): light = SmartBulb("123.123.123.123") @@ -412,7 +418,12 @@ async def test_unload(hass, platform): ), patch( f"homeassistant.components.tplink.{platform}.async_setup_entry", return_value=mock_coro(True), - ) as async_setup_entry: + ) as async_setup_entry, patch( + "homeassistant.components.tplink.get_time_offset", + return_value=( + datetime.now() - datetime.now().replace(hour=0, minute=0, second=0) + ), + ): config = { tplink.DOMAIN: { platform: [{CONF_HOST: "123.123.123.123"}], From 01082fb5ab2e2c0318bda9f4fb9b22ece2418355 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 18 Aug 2021 01:29:40 +0200 Subject: [PATCH 194/199] Fix tplink doing I/O in event loop and optimize (#54570) * Optimize tplink i/o * Cache has_emeter reduceing the number of i/o requests on hs300 by 5 * Use the state from the sysinfo dict for non-strips reducing required requests by one * Remove I/O from __init__, read has_emeter from sysinfo * Cleanup __init__ to avoid I/O * Re-use the sysinfo response for has_emeter * Use async_add_executor_job() to execute the synchronous I/O ops. * use the device alias instead of host for coordinator, use executor for unavailable_devices * Remove unnecessary self.hass assignment Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tplink/__init__.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 565876b1c7c..64637b9cdb5 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -161,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in unavailable_devices: try: - device.get_sysinfo() + await hass.async_add_executor_job(device.get_sysinfo) except SmartDeviceException: continue _LOGGER.debug( @@ -175,7 +175,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for switch in switches: try: - await hass.async_add_executor_job(switch.get_sysinfo) + info = await hass.async_add_executor_job(switch.get_sysinfo) except SmartDeviceException: _LOGGER.warning( "Device at '%s' not reachable during setup, will retry later", @@ -186,7 +186,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass_data[COORDINATORS][ switch.context or switch.mac - ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) + ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch, info["alias"]) await coordinator.async_config_entry_first_refresh() if unavailable_devices: @@ -222,16 +222,20 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, smartplug: SmartPlug, + alias: str, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" self.smartplug = smartplug update_interval = timedelta(seconds=30) super().__init__( - hass, _LOGGER, name=smartplug.alias, update_interval=update_interval + hass, + _LOGGER, + name=alias, + update_interval=update_interval, ) - async def _async_update_data(self) -> dict: + def _update_data(self) -> dict: """Fetch all device and sensor data from api.""" try: info = self.smartplug.sys_info @@ -244,9 +248,7 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): if self.smartplug.context is None: data[CONF_ALIAS] = info["alias"] data[CONF_DEVICE_ID] = info["mac"] - data[CONF_STATE] = ( - self.smartplug.state == self.smartplug.SWITCH_STATE_ON - ) + data[CONF_STATE] = bool(info["relay_state"]) else: plug_from_context = next( c @@ -256,7 +258,9 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): data[CONF_ALIAS] = plug_from_context["alias"] data[CONF_DEVICE_ID] = self.smartplug.context data[CONF_STATE] = plug_from_context["state"] == 1 - if self.smartplug.has_emeter: + + # Check if the device has emeter + if "ENE" in info["feature"]: emeter_readings = self.smartplug.get_emeter_realtime() data[CONF_EMETER_PARAMS] = { ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), @@ -288,3 +292,7 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): self.name = data[CONF_ALIAS] return data + + async def _async_update_data(self) -> dict: + """Fetch all device and sensor data from api.""" + return await self.hass.async_add_executor_job(self._update_data) From e783b8ce992ec24f38cd488c63e2cbcdd0dfac68 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 16 Aug 2021 16:13:07 +0200 Subject: [PATCH 195/199] Fix 'in' comparisons vesync light (#54614) --- homeassistant/components/vesync/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index b747c10ee4e..bd187f2f590 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -46,7 +46,7 @@ def _async_setup_entities(devices, async_add_entities): for dev in devices: if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): entities.append(VeSyncDimmableLightHA(dev)) - elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white"): + elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",): entities.append(VeSyncTunableWhiteLightHA(dev)) else: _LOGGER.debug( @@ -82,7 +82,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): """Turn the device on.""" attribute_adjustment_only = False # set white temperature - if self.color_mode in (COLOR_MODE_COLOR_TEMP) and ATTR_COLOR_TEMP in kwargs: + if self.color_mode in (COLOR_MODE_COLOR_TEMP,) and ATTR_COLOR_TEMP in kwargs: # get white temperature from HA data color_temp = int(kwargs[ATTR_COLOR_TEMP]) # ensure value between min-max supported Mireds From 99a9173d497f220598b65457ffe4606731f17237 Mon Sep 17 00:00:00 2001 From: Dylan Gore Date: Mon, 16 Aug 2021 12:52:40 +0100 Subject: [PATCH 196/199] Update PyMetEireann to 2021.8.0 (#54693) --- homeassistant/components/met_eireann/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json index 9d2e1857689..36cc905eabf 100644 --- a/homeassistant/components/met_eireann/manifest.json +++ b/homeassistant/components/met_eireann/manifest.json @@ -3,7 +3,7 @@ "name": "Met Éireann", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met_eireann", - "requirements": ["pyMetEireann==0.2"], + "requirements": ["pyMetEireann==2021.8.0"], "codeowners": ["@DylanGore"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2d026810c3f..bd0acf83d67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1272,7 +1272,7 @@ pyControl4==0.0.6 pyHS100==0.3.5.2 # homeassistant.components.met_eireann -pyMetEireann==0.2 +pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1619c2319c..fc5674fa546 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -714,7 +714,7 @@ pyControl4==0.0.6 pyHS100==0.3.5.2 # homeassistant.components.met_eireann -pyMetEireann==0.2 +pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air From be69191110837124e66b96397729285e85c99593 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 18 Aug 2021 17:58:07 +0200 Subject: [PATCH 197/199] Fix BMW remote services in rest_of_world & north_america (#54726) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 8131ac1415c..a7c4c5c837b 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.18"], + "requirements": ["bimmer_connected==0.7.19"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index bd0acf83d67..74b0e375d71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,7 +365,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.18 +bimmer_connected==0.7.19 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc5674fa546..67573604d73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -220,7 +220,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.18 +bimmer_connected==0.7.19 # homeassistant.components.blebox blebox_uniapi==1.3.3 From cfc2c40bd86cb27452b326c5c5219010e3ce8fd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Aug 2021 22:41:22 -0500 Subject: [PATCH 198/199] Fix HomeKit cover creation with tilt position, open/close, no set position (#54727) --- homeassistant/components/homekit/accessories.py | 7 ++++++- tests/components/homekit/test_get_accessories.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 836221ac7e9..7edeb7179eb 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -134,10 +134,15 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 and features & cover.SUPPORT_SET_POSITION ): a_type = "Window" - elif features & (cover.SUPPORT_SET_POSITION | cover.SUPPORT_SET_TILT_POSITION): + elif features & cover.SUPPORT_SET_POSITION: a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = "WindowCoveringBasic" + elif features & cover.SUPPORT_SET_TILT_POSITION: + # WindowCovering and WindowCoveringBasic both support tilt + # only WindowCovering can handle the covers that are missing + # SUPPORT_SET_POSITION, SUPPORT_OPEN, and SUPPORT_CLOSE + a_type = "WindowCovering" elif state.domain == "fan": a_type = "Fan" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 1b220153195..af98f6a45f9 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -149,6 +149,18 @@ def test_types(type_name, entity_id, state, attrs, config): "open", {ATTR_SUPPORTED_FEATURES: (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE)}, ), + ( + "WindowCoveringBasic", + "cover.open_window", + "open", + { + ATTR_SUPPORTED_FEATURES: ( + cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_SET_TILT_POSITION + ) + }, + ), ], ) def test_type_covers(type_name, entity_id, state, attrs): From a46b39d3c19964155dc944ea7cac1aa031f9ee84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Aug 2021 13:53:12 -0700 Subject: [PATCH 199/199] Bumped version to 2021.8.8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c96f621ade2..667c9c11389 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "7" +PATCH_VERSION: Final = "8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)