diff --git a/.coveragerc b/.coveragerc index 1a9221b3abb..fd4f87da858 100644 --- a/.coveragerc +++ b/.coveragerc @@ -36,6 +36,9 @@ omit = homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py + homeassistant/components/airtouch4/__init__.py + homeassistant/components/airtouch4/climate.py + homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/* @@ -375,6 +378,7 @@ omit = homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py + homeassistant/components/google_pubsub/__init__.py homeassistant/components/google_travel_time/__init__.py homeassistant/components/google_travel_time/helpers.py homeassistant/components/google_travel_time/sensor.py @@ -666,17 +670,19 @@ omit = homeassistant/components/mysensors/helpers.py homeassistant/components/mysensors/light.py homeassistant/components/mysensors/notify.py - homeassistant/components/mysensors/sensor.py homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py homeassistant/components/myq/__init__.py + homeassistant/components/myq/cover.py + homeassistant/components/myq/light.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py homeassistant/components/neato/camera.py + homeassistant/components/neato/hub.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py @@ -695,7 +701,8 @@ omit = homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* - homeassistant/components/nmap_tracker/* + homeassistant/components/nmap_tracker/__init__.py + homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py @@ -1114,10 +1121,6 @@ omit = homeassistant/components/upcloud/switch.py homeassistant/components/upnp/* homeassistant/components/upc_connect/* - homeassistant/components/uptimerobot/__init__.py - homeassistant/components/uptimerobot/binary_sensor.py - homeassistant/components/uptimerobot/const.py - homeassistant/components/uptimerobot/entity.py homeassistant/components/uscis/sensor.py homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py @@ -1201,6 +1204,7 @@ omit = homeassistant/components/xiaomi_miio/__init__.py homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/alarm_control_panel.py + homeassistant/components/xiaomi_miio/binary_sensor.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7c169580cb2..974022834fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -71,6 +71,7 @@ If the code communicates with devices, web services, or third-party tools: Updated and included derived files by running: `python3 -m script.hassfest`. - [ ] New or updated dependencies have been added to `requirements_all.txt`. Updated by running `python3 -m script.gen_requirements_all`. +- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description. - [ ] Untested files have been added to `.coveragerc`. The integration reached or maintains the following [Integration Quality Scale][quality-scale]: diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index abe0cfcb63e..25d4d0ca8a0 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -248,11 +248,12 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Install VCN tools + uses: home-assistant/actions/helpers/vcn@master + - name: Build Meta Image shell: bash run: | - bash <(curl https://getvcn.codenotary.com -L) - export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 62c7299c2b8..96fc69e3b68 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2.1.1 + - uses: dessant/lock-threads@v2.1.2 with: github-token: ${{ github.token }} issue-lock-inactive-days: "30" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e36ae652d6..38ba2a503af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.23.0 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py38-plus] @@ -45,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.8.0 + rev: 5.9.3 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/.strict-typing b/.strict-typing index 915ac50d6a1..e8c4f83fa80 100644 --- a/.strict-typing +++ b/.strict-typing @@ -63,6 +63,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* diff --git a/CODEOWNERS b/CODEOWNERS index 86622690fb9..3606fade468 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,6 +29,7 @@ homeassistant/components/aemet/* @noltari homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu homeassistant/components/airnow/* @asymworks +homeassistant/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy @@ -187,6 +188,7 @@ homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu +homeassistant/components/github/* @timmo001 homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob @@ -320,7 +322,7 @@ homeassistant/components/msteams/* @peroyvind homeassistant/components/mullvad/* @meichthys homeassistant/components/mutesync/* @currentoor homeassistant/components/my/* @home-assistant/core -homeassistant/components/myq/* @bdraco +homeassistant/components/myq/* @bdraco @ehendrix23 homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff homeassistant/components/nam/* @bieniu @@ -338,6 +340,7 @@ homeassistant/components/nfandroidtv/* @tkdrob homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole +homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 @@ -503,7 +506,7 @@ homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/system_bridge/* @timmo001 -homeassistant/components/tado/* @michaelarnauts @bdraco @noltari +homeassistant/components/tado/* @michaelarnauts @noltari homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages @@ -527,6 +530,7 @@ homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus homeassistant/components/trace/* @home-assistant/core homeassistant/components/tractive/* @Danielhiversen @zhulik +homeassistant/components/tradfri/* @janiversen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins @@ -541,7 +545,7 @@ homeassistant/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core -homeassistant/components/upnp/* @StevenLooman +homeassistant/components/upnp/* @StevenLooman @ehendrix23 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes @@ -584,7 +588,7 @@ homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yale_smart_alarm/* @gjohansson-ST homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn +homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 0b360668ad4..63cbeb1bf7e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -17,6 +17,8 @@ from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType +# mypy: disallow-any-generics + STORAGE_VERSION = 1 STORAGE_KEY = "auth" GROUP_NAME_ADMIN = "Administrators" @@ -491,7 +493,7 @@ class AuthStore: self._store.async_delay_save(self._data_to_save, 1) @callback - def _data_to_save(self) -> dict: + def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return the data to store.""" assert self._users is not None assert self._groups is not None diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index d2dfa0e1c6d..4faa277a081 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -22,6 +22,8 @@ from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION from ..models import Credentials, RefreshToken, User, UserMeta +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -96,7 +98,7 @@ class AuthProvider: # Implement by extending class - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return the data flow for logging in with auth provider. Auth provider should extend LoginFlow and return an instance. diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index f462ad4be9d..6d1a1627fd5 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -17,6 +17,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + CONF_ARGS = "args" CONF_META = "meta" @@ -56,7 +58,7 @@ class CommandLineAuthProvider(AuthProvider): super().__init__(*args, **kwargs) self._user_meta: dict[str, dict[str, Any]] = {} - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return CommandLineLoginFlow(self) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index dfbf077a89d..b08c59bf3aa 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -19,6 +19,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" @@ -235,7 +237,7 @@ class HassAuthProvider(AuthProvider): await data.async_load() self.data = data - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return HassLoginFlow(self) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 5a3a890ff66..fb390b65b0d 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping import hmac -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -15,6 +15,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + USER_SCHEMA = vol.Schema( { vol.Required("username"): str, @@ -37,7 +39,7 @@ class InvalidAuthError(HomeAssistantError): class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return ExampleLoginFlow(self) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index b385aa0ed59..af24506210b 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -7,7 +7,7 @@ from __future__ import annotations from collections.abc import Mapping import hmac -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -19,6 +19,8 @@ import homeassistant.helpers.config_validation as cv from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" @@ -44,7 +46,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): """Return api_password.""" return str(self.config[CONF_API_PASSWORD]) - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return LegacyLoginFlow(self) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 7b609f371ef..a9ee6a48335 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -27,6 +27,8 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import InvalidAuthError from ..models import Credentials, RefreshToken, UserMeta +# mypy: disallow-any-generics + IPAddress = Union[IPv4Address, IPv6Address] IPNetwork = Union[IPv4Network, IPv6Network] @@ -97,7 +99,7 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" assert context is not None ip_addr = cast(IPAddress, context.get("ip_address")) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 99d4fd433a7..987e32f9911 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -1,4 +1,6 @@ """Support for Abode Security System cameras.""" +from __future__ import annotations + from datetime import timedelta import abodepy.helpers.constants as CONST @@ -73,7 +75,9 @@ class AbodeCamera(AbodeDevice, Camera): else: self._response = None - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get a camera image.""" self.refresh_image() diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index f1f744a5511..03687fc3907 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,7 +1,9 @@ """Support for Abode Security System sensors.""" +from __future__ import annotations + import abodepy.helpers.constants as CONST -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -11,12 +13,23 @@ from homeassistant.const import ( from . import AbodeDevice from .const import DOMAIN -# Sensor types: Name, icon -SENSOR_TYPES = { - CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE], - CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY], - CONST.LUX_STATUS_KEY: ["Lux", DEVICE_CLASS_ILLUMINANCE], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=CONST.TEMP_STATUS_KEY, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=CONST.HUMI_STATUS_KEY, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + ), + SensorEntityDescription( + key=CONST.LUX_STATUS_KEY, + name="Lux", + device_class=DEVICE_CLASS_ILLUMINANCE, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -26,10 +39,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - for sensor_type in SENSOR_TYPES: - if sensor_type not in device.get_value(CONST.STATUSES_KEY): - continue - entities.append(AbodeSensor(data, device, sensor_type)) + conditions = device.get_value(CONST.STATUSES_KEY) + entities.extend( + [ + AbodeSensor(data, device, description) + for description in SENSOR_TYPES + if description.key in conditions + ] + ) async_add_entities(entities) @@ -37,26 +54,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" - def __init__(self, data, device, sensor_type): + def __init__(self, data, device, description: SensorEntityDescription): """Initialize a sensor for an Abode device.""" super().__init__(data, device) - self._sensor_type = sensor_type - self._attr_name = f"{device.name} {SENSOR_TYPES[sensor_type][0]}" - self._attr_device_class = SENSOR_TYPES[self._sensor_type][1] - self._attr_unique_id = f"{device.device_uuid}-{sensor_type}" - if self._sensor_type == CONST.TEMP_STATUS_KEY: - self._attr_unit_of_measurement = device.temp_unit - elif self._sensor_type == CONST.HUMI_STATUS_KEY: - self._attr_unit_of_measurement = device.humidity_unit - elif self._sensor_type == CONST.LUX_STATUS_KEY: - self._attr_unit_of_measurement = device.lux_unit + self.entity_description = description + self._attr_name = f"{device.name} {description.name}" + self._attr_unique_id = f"{device.device_uuid}-{description.key}" + if description.key == CONST.TEMP_STATUS_KEY: + self._attr_native_unit_of_measurement = device.temp_unit + elif description.key == CONST.HUMI_STATUS_KEY: + self._attr_native_unit_of_measurement = device.humidity_unit + elif description.key == CONST.LUX_STATUS_KEY: + self._attr_native_unit_of_measurement = device.lux_unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" - if self._sensor_type == CONST.TEMP_STATUS_KEY: + if self.entity_description.key == CONST.TEMP_STATUS_KEY: return self._device.temp - if self._sensor_type == CONST.HUMI_STATUS_KEY: + if self.entity_description.key == CONST.HUMI_STATUS_KEY: return self._device.humidity - if self._sensor_type == CONST.LUX_STATUS_KEY: + if self.entity_description.key == CONST.LUX_STATUS_KEY: return self._device.lux diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 4a5af6054e1..b5f979b45cf 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -88,10 +88,10 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): ) if coordinator.is_metric: self._unit_system = API_METRIC - self._attr_unit_of_measurement = description.unit_metric + self._attr_native_unit_of_measurement = description.unit_metric else: self._unit_system = API_IMPERIAL - self._attr_unit_of_measurement = description.unit_imperial + self._attr_native_unit_of_measurement = description.unit_imperial self._attr_device_info = { "identifiers": {(DOMAIN, coordinator.location_key)}, "name": NAME, @@ -101,7 +101,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self.forecast_day = forecast_day @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" if self.forecast_day is not None: if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index ce4721693f3..7b4d270f78b 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "requests_exceeded": "T\u00fall\u00e9pt\u00e9k az Accuweather API-hoz beny\u00fajtott k\u00e9relmek megengedett sz\u00e1m\u00e1t. Meg kell v\u00e1rnia vagy m\u00f3dos\u00edtania kell az API-kulcsot." }, "step": { "user": { @@ -15,6 +16,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, + "description": "Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1l\u00e1shoz, n\u00e9zze meg itt: https://www.home-assistant.io/integrations/accuweather/ \n\nEgyes \u00e9rz\u00e9kel\u0151k alap\u00e9rtelmez\u00e9s szerint nincsenek enged\u00e9lyezve. Az integr\u00e1ci\u00f3s konfigur\u00e1ci\u00f3 ut\u00e1n enged\u00e9lyezheti \u0151ket az entit\u00e1s-nyilv\u00e1ntart\u00e1sban.\nAz id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9s alap\u00e9rtelmez\u00e9s szerint nincs enged\u00e9lyezve. Ezt az integr\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sokban enged\u00e9lyezheti.", "title": "AccuWeather" } } @@ -22,6 +24,10 @@ "options": { "step": { "user": { + "data": { + "forecast": "Id\u0151j\u00e1r\u00e1s el\u0151rejelz\u00e9s" + }, + "description": "Az AccuWeather API kulcs ingyenes verzi\u00f3j\u00e1nak korl\u00e1tai miatt, amikor enged\u00e9lyezi az id\u0151j\u00e1r\u00e1s -el\u0151rejelz\u00e9st, az adatfriss\u00edt\u00e9seket 40 percenk\u00e9nt 80 percenk\u00e9nt hajtj\u00e1k v\u00e9gre.", "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 4f617c5726f..43f5e32c74f 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -34,7 +34,7 @@ class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" device_class = DEVICE_CLASS_BATTERY - unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def name(self): @@ -42,6 +42,6 @@ class AcmedaBattery(AcmedaBase, SensorEntity): return f"{super().name} Battery" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.roller.battery diff --git a/homeassistant/components/acmeda/translations/hu.json b/homeassistant/components/acmeda/translations/hu.json index 6105977de80..f302995e7e9 100644 --- a/homeassistant/components/acmeda/translations/hu.json +++ b/homeassistant/components/acmeda/translations/hu.json @@ -2,6 +2,14 @@ "config": { "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "step": { + "user": { + "data": { + "id": "Gazdag\u00e9p azonos\u00edt\u00f3" + }, + "title": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt hubot" + } } } } \ No newline at end of file diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 74e973ba6d5..1abd83fdbfc 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -49,20 +49,19 @@ async def async_setup_entry( class AdaxDevice(ClimateEntity): """Representation of a heater.""" + _attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + _attr_max_temp = 35 + _attr_min_temp = 5 + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" self._heater_data = heater_data self._adax_data_handler = adax_data_handler - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._heater_data['homeId']}_{self._heater_data['id']}" + self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" @property def name(self) -> str: @@ -83,11 +82,6 @@ class AdaxDevice(ClimateEntity): return "mdi:radiator" return "mdi:radiator-off" - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] - async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: @@ -105,21 +99,6 @@ class AdaxDevice(ClimateEntity): return await self._adax_data_handler.update() - @property - def temperature_unit(self) -> str: - """Return the unit of measurement which this device uses.""" - return TEMP_CELSIUS - - @property - def min_temp(self) -> int: - """Return the minimum temperature.""" - return 5 - - @property - def max_temp(self) -> int: - """Return the maximum temperature.""" - return 35 - @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -130,11 +109,6 @@ class AdaxDevice(ClimateEntity): """Return the temperature we try to reach.""" return self._heater_data.get("targetTemperature") - @property - def target_temperature_step(self) -> int: - """Return the supported step of target temperature.""" - return PRECISION_WHOLE - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) 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/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json index ce5fa77543f..1d090f44de2 100644 --- a/homeassistant/components/adax/translations/cs.json +++ b/homeassistant/components/adax/translations/cs.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "account_id": "ID \u00fa\u010dtu", "host": "Hostitel", "password": "Heslo" } diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json new file mode 100644 index 00000000000..4a65e469bcd --- /dev/null +++ b/homeassistant/components/adax/translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "account_id": "ID de la cuenta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json new file mode 100644 index 00000000000..726381a4dd7 --- /dev/null +++ b/homeassistant/components/adax/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "account_id": "Fi\u00f3k ID", + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/no.json b/homeassistant/components/adax/translations/no.json new file mode 100644 index 00000000000..33c54b57093 --- /dev/null +++ b/homeassistant/components/adax/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "account_id": "Konto-ID", + "host": "Vert", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/zh-Hans.json b/homeassistant/components/adax/translations/zh-Hans.json new file mode 100644 index 00000000000..7356ec08b15 --- /dev/null +++ b/homeassistant/components/adax/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 7499cf51d0c..a7f4dabde9f 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -82,12 +82,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 22fb5539bfa..8a860caf79d 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "existing_instance_updated": "Friss\u00edtette a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3t." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" @@ -19,7 +20,8 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "description": "\u00c1ll\u00edtsa be az AdGuard Home p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a fel\u00fcgyeletet \u00e9s az ir\u00e1ny\u00edt\u00e1st." } } } diff --git a/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant/components/adguard/translations/zh-Hans.json index 4204beb5268..ee68ce83e91 100644 --- a/homeassistant/components/adguard/translations/zh-Hans.json +++ b/homeassistant/components/adguard/translations/zh-Hans.json @@ -1,14 +1,23 @@ { "config": { "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", "existing_instance_updated": "\u66f4\u65b0\u4e86\u73b0\u6709\u914d\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, "step": { "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66\u51ed\u8bc1", + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66\u51ed\u8bc1" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 AdGuard Home \u5b9e\u4f8b\u4ee5\u5141\u8bb8\u76d1\u89c6\u548c\u63a7\u5236" } } } 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 diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index fe68c4c860b..26b04d86050 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -50,7 +50,7 @@ class AdsSensor(AdsEntity, SensorEntity): def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): """Initialize AdsSensor entity.""" super().__init__(ads_hub, name, ads_var) - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._ads_type = ads_type self._factor = factor @@ -64,6 +64,6 @@ class AdsSensor(AdsEntity, SensorEntity): ) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the device.""" return self._state_dict[STATE_KEY_STATE] 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.""" diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index eca7651d6eb..5912101fd65 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" - _attr_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" @@ -58,7 +58,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value.""" return self._ac[self._time_key] @@ -78,7 +78,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, instance, ac_key, zone_key): @@ -90,7 +90,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value of the air vent.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return self._zone["value"] @@ -107,19 +107,19 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" - super().__init__(instance, ac_key, zone_key=zone_key) + super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} Signal' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' ) @property - def state(self): + def native_value(self): """Return the current value of the wireless signal.""" return self._zone["rssi"] @@ -140,7 +140,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = STATE_CLASS_MEASUREMENT _attr_icon = "mdi:thermometer" _attr_entity_registry_enabled_default = False @@ -149,9 +149,11 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} Temperature' - self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' + ) @property - def state(self): + def native_value(self): """Return the current value of the measured temperature.""" return self._zone["measuredTemp"] diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 3fd0769cb00..35336980e1a 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -85,7 +85,7 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{self._name} {self._sensor_name}" self._attr_unique_id = self._unique_id self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) - self._attr_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._attr_native_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) class AemetSensor(AbstractAemetSensor): @@ -106,7 +106,7 @@ class AemetSensor(AbstractAemetSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._weather_coordinator.data.get(self._sensor_type) @@ -134,7 +134,7 @@ class AemetForecastSensor(AbstractAemetSensor): ) @property - def state(self): + def native_value(self): """Return the state of the device.""" forecast = None forecasts = self._weather_coordinator.data.get( diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index fd8e095f65f..a3b41f8314c 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -109,7 +109,7 @@ async def async_setup_platform( class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" - _attr_unit_of_measurement: str = "packages" + _attr_native_unit_of_measurement: str = "packages" _attr_icon: str = ICON def __init__(self, aftership: Tracking, name: str) -> None: @@ -120,7 +120,7 @@ class AfterShipSensor(SensorEntity): self._attr_name = name @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._state 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.""" diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index 49968ceea75..fff86517073 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -12,7 +12,8 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "\u00c1ll\u00edtsa be az Agent DVR-t" } } } diff --git a/homeassistant/components/agent_dvr/translations/zh-Hans.json b/homeassistant/components/agent_dvr/translations/zh-Hans.json index 2941dfd9383..68393fce470 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hans.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hans.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, "error": { + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u8fdb\u884c\u4e2d", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3" + }, + "title": "\u914d\u7f6e Agent DVR" + } } } } \ No newline at end of file diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 157f28c33f7..79004abbe41 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -6,7 +6,11 @@ from typing import Final from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -49,35 +53,36 @@ NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, + device_class=DEVICE_CLASS_AQI, name=ATTR_API_CAQI, - unit_of_measurement="CAQI", + native_unit_of_measurement="CAQI", ), AirlySensorEntityDescription( key=ATTR_API_PM1, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM1, name=ATTR_API_PM1, - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM25, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM25, name="PM2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM10, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM10, name=ATTR_API_PM10, - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY, name=ATTR_API_HUMIDITY.capitalize(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, value=lambda value: round(value, 1), ), @@ -85,14 +90,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( key=ATTR_API_PRESSURE, device_class=DEVICE_CLASS_PRESSURE, name=ATTR_API_PRESSURE.capitalize(), - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE, name=ATTR_API_TEMPERATURE.capitalize(), - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, state_class=STATE_CLASS_MEASUREMENT, value=lambda value: round(value, 1), ), diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 2c811b00aa6..b5d45afd2d8 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -84,7 +84,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" state = self.coordinator.data[self.entity_description.key] return cast(StateType, self.entity_description.value(state)) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 31ea5e298e3..a0f8d7e701b 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -72,11 +72,11 @@ class AirNowSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON] self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - self._attr_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] + self._attr_native_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" @property - def state(self): + def native_value(self): """Return the state.""" self._state = self.coordinator.data[self.kind] return self._state diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py new file mode 100644 index 00000000000..0ec63161ea3 --- /dev/null +++ b/homeassistant/components/airtouch4/__init__.py @@ -0,0 +1,81 @@ +"""The AirTouch4 integration.""" +import logging + +from airtouch4pyapi import AirTouch +from airtouch4pyapi.airtouch import AirTouchStatus + +from homeassistant.components.climate import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up AirTouch4 from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + host = entry.data[CONF_HOST] + airtouch = AirTouch(host) + await airtouch.UpdateInfo() + info = airtouch.GetAcs() + if not info: + raise ConfigEntryNotReady + coordinator = AirtouchDataUpdateCoordinator(hass, airtouch) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class AirtouchDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Airtouch data.""" + + def __init__(self, hass, airtouch): + """Initialize global Airtouch data updater.""" + self.airtouch = airtouch + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Fetch data from Airtouch.""" + await self.airtouch.UpdateInfo() + if self.airtouch.Status != AirTouchStatus.OK: + raise UpdateFailed("Airtouch connection issue") + return { + "acs": [ + {"ac_number": ac.AcNumber, "is_on": ac.IsOn} + for ac in self.airtouch.GetAcs() + ], + "groups": [ + { + "group_number": group.GroupNumber, + "group_name": group.GroupName, + "is_on": group.IsOn, + } + for group in self.airtouch.GetGroups() + ], + } diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py new file mode 100644 index 00000000000..7202feb0527 --- /dev/null +++ b/homeassistant/components/airtouch4/climate.py @@ -0,0 +1,335 @@ +"""AirTouch 4 component to control of AirTouch 4 Climate Devices.""" + +import logging + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE +AT_TO_HA_STATE = { + "Heat": HVAC_MODE_HEAT, + "Cool": HVAC_MODE_COOL, + "AutoHeat": HVAC_MODE_AUTO, # airtouch reports either autoheat or autocool + "AutoCool": HVAC_MODE_AUTO, + "Auto": HVAC_MODE_AUTO, + "Dry": HVAC_MODE_DRY, + "Fan": HVAC_MODE_FAN_ONLY, +} + +HA_STATE_TO_AT = { + HVAC_MODE_HEAT: "Heat", + HVAC_MODE_COOL: "Cool", + HVAC_MODE_AUTO: "Auto", + HVAC_MODE_DRY: "Dry", + HVAC_MODE_FAN_ONLY: "Fan", + HVAC_MODE_OFF: "Off", +} + +AT_TO_HA_FAN_SPEED = { + "Quiet": FAN_DIFFUSE, + "Low": FAN_LOW, + "Medium": FAN_MEDIUM, + "High": FAN_HIGH, + "Powerful": FAN_FOCUS, + "Auto": FAN_AUTO, + "Turbo": "turbo", +} + +AT_GROUP_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + +HA_FAN_SPEED_TO_AT = {value: key for key, value in AT_TO_HA_FAN_SPEED.items()} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Airtouch 4.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + info = coordinator.data + entities = [ + AirtouchGroup(coordinator, group["group_number"], info) + for group in info["groups"] + ] + [AirtouchAC(coordinator, ac["ac_number"], info) for ac in info["acs"]] + + _LOGGER.debug(" Found entities %s", entities) + + async_add_entities(entities) + + +class AirtouchAC(CoordinatorEntity, ClimateEntity): + """Representation of an AirTouch 4 ac.""" + + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, coordinator, ac_number, info): + """Initialize the climate device.""" + super().__init__(coordinator) + self._ac_number = ac_number + self._airtouch = coordinator.airtouch + self._info = info + self._unit = self._airtouch.GetAcs()[self._ac_number] + + @callback + def _handle_coordinator_update(self): + self._unit = self._airtouch.GetAcs()[self._ac_number] + return super()._handle_coordinator_update() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Airtouch", + "model": "Airtouch 4", + } + + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"ac_{self._ac_number}" + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._unit.Temperature + + @property + def name(self): + """Return the name of the climate device.""" + return f"AC {self._ac_number}" + + @property + def fan_mode(self): + """Return fan mode of the AC this group belongs to.""" + return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed] + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number) + return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + is_off = self._unit.PowerState == "Off" + if is_off: + return HVAC_MODE_OFF + + return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode] + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number) + modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes] + modes.append(HVAC_MODE_OFF) + return modes + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + if hvac_mode not in HA_STATE_TO_AT: + raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") + + if hvac_mode == HVAC_MODE_OFF: + return await self.async_turn_off() + await self._airtouch.SetCoolingModeForAc( + self._ac_number, HA_STATE_TO_AT[hvac_mode] + ) + # in case it isn't already, unless the HVAC mode was off, then the ac should be on + await self.async_turn_on() + self._unit = self._airtouch.GetAcs()[self._ac_number] + _LOGGER.debug("Setting operation mode of %s to %s", self._ac_number, hvac_mode) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if fan_mode not in self.fan_modes: + raise ValueError(f"Unsupported fan mode: {fan_mode}") + + _LOGGER.debug("Setting fan mode of %s to %s", self._ac_number, fan_mode) + await self._airtouch.SetFanSpeedForAc( + self._ac_number, HA_FAN_SPEED_TO_AT[fan_mode] + ) + self._unit = self._airtouch.GetAcs()[self._ac_number] + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + # in case ac is not on. Airtouch turns itself off if no groups are turned on + # (even if groups turned back on) + await self._airtouch.TurnAcOn(self._ac_number) + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + await self._airtouch.TurnAcOff(self._ac_number) + self.async_write_ha_state() + + +class AirtouchGroup(CoordinatorEntity, ClimateEntity): + """Representation of an AirTouch 4 group.""" + + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS + _attr_hvac_modes = AT_GROUP_MODES + + def __init__(self, coordinator, group_number, info): + """Initialize the climate device.""" + super().__init__(coordinator) + self._group_number = group_number + self._airtouch = coordinator.airtouch + self._info = info + self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) + + @callback + def _handle_coordinator_update(self): + self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) + return super()._handle_coordinator_update() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Airtouch", + "model": "Airtouch 4", + } + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._group_number + + @property + def min_temp(self): + """Return Minimum Temperature for AC of this group.""" + return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint + + @property + def max_temp(self): + """Return Max Temperature for AC of this group.""" + return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint + + @property + def name(self): + """Return the name of the climate device.""" + return self._unit.GroupName + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._unit.Temperature + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + return self._unit.TargetSetpoint + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + # there are other power states that aren't 'on' but still count as on (eg. 'Turbo') + is_off = self._unit.PowerState == "Off" + if is_off: + return HVAC_MODE_OFF + + return HVAC_MODE_FAN_ONLY + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + if hvac_mode not in HA_STATE_TO_AT: + raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") + + if hvac_mode == HVAC_MODE_OFF: + return await self.async_turn_off() + if self.hvac_mode == HVAC_MODE_OFF: + await self.async_turn_on() + self._unit = self._airtouch.GetGroups()[self._group_number] + _LOGGER.debug( + "Setting operation mode of %s to %s", self._group_number, hvac_mode + ) + self.async_write_ha_state() + + @property + def fan_mode(self): + """Return fan mode of the AC this group belongs to.""" + return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed] + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup( + self._group_number + ) + return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] + + async def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + _LOGGER.debug("Setting temp of %s to %s", self._group_number, str(temp)) + self._unit = await self._airtouch.SetGroupToTemperature( + self._group_number, int(temp) + ) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if fan_mode not in self.fan_modes: + raise ValueError(f"Unsupported fan mode: {fan_mode}") + + _LOGGER.debug("Setting fan mode of %s to %s", self._group_number, fan_mode) + self._unit = await self._airtouch.SetFanSpeedByGroup( + self._group_number, HA_FAN_SPEED_TO_AT[fan_mode] + ) + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + await self._airtouch.TurnGroupOn(self._group_number) + + # in case ac is not on. Airtouch turns itself off if no groups are turned on + # (even if groups turned back on) + await self._airtouch.TurnAcOn( + self._airtouch.GetGroupByGroupNumber(self._group_number).BelongsToAc + ) + # this might cause the ac object to be wrong, so force the shared data + # store to update + await self.coordinator.async_request_refresh() + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + await self._airtouch.TurnGroupOff(self._group_number) + # this will cause the ac object to be wrong + # (ac turns off automatically if no groups are running) + # so force the shared data store to update + await self.coordinator.async_request_refresh() + self.async_write_ha_state() diff --git a/homeassistant/components/airtouch4/config_flow.py b/homeassistant/components/airtouch4/config_flow.py new file mode 100644 index 00000000000..e395c71349b --- /dev/null +++ b/homeassistant/components/airtouch4/config_flow.py @@ -0,0 +1,50 @@ +"""Config flow for AirTouch4.""" +from airtouch4pyapi import AirTouch, AirTouchStatus +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST + +from .const import DOMAIN + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + + +class AirtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an Airtouch config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) + + airtouch = AirTouch(host) + await airtouch.UpdateInfo() + airtouch_status = airtouch.Status + airtouch_has_groups = bool( + airtouch.Status == AirTouchStatus.OK and airtouch.GetGroups() + ) + + if airtouch_status != AirTouchStatus.OK: + errors["base"] = "cannot_connect" + elif not airtouch_has_groups: + errors["base"] = "no_units" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self.async_create_entry( + title=user_input[CONF_HOST], + data={ + CONF_HOST: user_input[CONF_HOST], + }, + ) diff --git a/homeassistant/components/airtouch4/const.py b/homeassistant/components/airtouch4/const.py new file mode 100644 index 00000000000..e110a6cee81 --- /dev/null +++ b/homeassistant/components/airtouch4/const.py @@ -0,0 +1,3 @@ +"""Constants for the AirTouch4 integration.""" + +DOMAIN = "airtouch4" diff --git a/homeassistant/components/airtouch4/manifest.json b/homeassistant/components/airtouch4/manifest.json new file mode 100644 index 00000000000..8297081ae9d --- /dev/null +++ b/homeassistant/components/airtouch4/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "airtouch4", + "name": "AirTouch 4", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airtouch4", + "requirements": [ + "airtouch4pyapi==1.0.5" + ], + "codeowners": [ + "@LonePurpleWolf" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/strings.json b/homeassistant/components/airtouch4/strings.json new file mode 100644 index 00000000000..5259b20fb73 --- /dev/null +++ b/homeassistant/components/airtouch4/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_units": "Could not find any AirTouch 4 Groups." + }, + "step": { + "user": { + "title": "Setup your AirTouch 4 connection details.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + } + } +} diff --git a/homeassistant/components/airtouch4/translations/ca.json b/homeassistant/components/airtouch4/translations/ca.json new file mode 100644 index 00000000000..083c4a0ba87 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_units": "No s'han trobat grups AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Configura els detalls de connexi\u00f3 d'AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/cs.json b/homeassistant/components/airtouch4/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/de.json b/homeassistant/components/airtouch4/translations/de.json new file mode 100644 index 00000000000..84f93d09962 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_units": "Es konnten keine AirTouch 4-Gruppen gefunden werden." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Richte deine AirTouch 4-Verbindungsdetails ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/en.json b/homeassistant/components/airtouch4/translations/en.json new file mode 100644 index 00000000000..0f86b787249 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "no_units": "Could not find any AirTouch 4 Groups." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Setup your AirTouch 4 connection details." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/et.json b/homeassistant/components/airtouch4/translations/et.json new file mode 100644 index 00000000000..2b42935b18e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_units": "Ei leidnud \u00fchtegi AirTouch 4 gruppi." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "AirTouch 4 \u00fchenduse \u00fcksikasjade seadistamine." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/it.json b/homeassistant/components/airtouch4/translations/it.json new file mode 100644 index 00000000000..f9a72a50e33 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "no_units": "Impossibile trovare alcun gruppo AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Imposta i dettagli della connessione AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/nl.json b/homeassistant/components/airtouch4/translations/nl.json new file mode 100644 index 00000000000..d6137499b3e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "no_units": "Kan geen AirTouch 4-groepen vinden." + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/ru.json b/homeassistant/components/airtouch4/translations/ru.json new file mode 100644 index 00000000000..cbb7b10de79 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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.", + "no_units": "\u0413\u0440\u0443\u043f\u043f\u044b AirTouch 4 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "AirTouch 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/zh-Hant.json b/homeassistant/components/airtouch4/translations/zh-Hant.json new file mode 100644 index 00000000000..9ac310b531b --- /dev/null +++ b/homeassistant/components/airtouch4/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_units": "\u627e\u4e0d\u5230\u4efb\u4f55 AirTouch 4 \u7fa4\u7d44\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u8a2d\u5b9a AirTouch 4 \u9023\u7dda\u8cc7\u8a0a\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 2f8dd07c625..922c84357ae 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -212,7 +212,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): self._attr_icon = icon self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {name}" self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{kind}" - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._config_entry = config_entry self._kind = kind self._locale = locale @@ -232,16 +232,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - [(self._attr_state, self._attr_icon)] = [ + [(self._attr_native_value, self._attr_icon)] = [ (name, icon) for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items() if floor <= aqi <= ceiling ] elif self._kind == SENSOR_KIND_AQI: - self._attr_state = data[f"aqi{self._locale}"] + self._attr_native_value = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._attr_state = symbol + self._attr_native_value = symbol self._attr_extra_state_attributes.update( { ATTR_POLLUTANT_SYMBOL: symbol, @@ -298,7 +298,7 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): f"{coordinator.data['settings']['node_name']} Node/Pro: {name}" ) self._attr_unique_id = f"{coordinator.data['serial_number']}_{kind}" - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._kind = kind @property @@ -320,24 +320,30 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_AQI: if self.coordinator.data["settings"]["is_aqi_usa"]: - self._attr_state = self.coordinator.data["measurements"]["aqi_us"] + self._attr_native_value = self.coordinator.data["measurements"][ + "aqi_us" + ] else: - self._attr_state = self.coordinator.data["measurements"]["aqi_cn"] + self._attr_native_value = self.coordinator.data["measurements"][ + "aqi_cn" + ] elif self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._attr_state = self.coordinator.data["status"]["battery"] + self._attr_native_value = self.coordinator.data["status"]["battery"] elif self._kind == SENSOR_KIND_CO2: - self._attr_state = self.coordinator.data["measurements"].get("co2") + self._attr_native_value = self.coordinator.data["measurements"].get("co2") elif self._kind == SENSOR_KIND_HUMIDITY: - self._attr_state = self.coordinator.data["measurements"].get("humidity") + self._attr_native_value = self.coordinator.data["measurements"].get( + "humidity" + ) elif self._kind == SENSOR_KIND_PM_0_1: - self._attr_state = self.coordinator.data["measurements"].get("pm0_1") + self._attr_native_value = self.coordinator.data["measurements"].get("pm0_1") elif self._kind == SENSOR_KIND_PM_1_0: - self._attr_state = self.coordinator.data["measurements"].get("pm1_0") + self._attr_native_value = self.coordinator.data["measurements"].get("pm1_0") elif self._kind == SENSOR_KIND_PM_2_5: - self._attr_state = self.coordinator.data["measurements"].get("pm2_5") + self._attr_native_value = self.coordinator.data["measurements"].get("pm2_5") elif self._kind == SENSOR_KIND_TEMPERATURE: - self._attr_state = self.coordinator.data["measurements"].get( + self._attr_native_value = self.coordinator.data["measurements"].get( "temperature_C" ) elif self._kind == SENSOR_KIND_VOC: - self._attr_state = self.coordinator.data["measurements"].get("voc") + self._attr_native_value = self.coordinator.data["measurements"].get("voc") diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index e7c47e93793..043a2402283 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -34,13 +34,29 @@ "data": { "ip_address": "Hoszt", "password": "Jelsz\u00f3" - } + }, + "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", + "title": "AirVisual Node/Pro konfigur\u00e1l\u00e1sa" }, "reauth_confirm": { "data": { "api_key": "API kulcs" }, "title": "Az AirVisual \u00fajb\u00f3li hiteles\u00edt\u00e9se" + }, + "user": { + "description": "V\u00e1lassza ki, hogy milyen t\u00edpus\u00fa AirVisual adatokat szeretne figyelni.", + "title": "Az AirVisual konfigur\u00e1l\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "A megfigyelt f\u00f6ldrajz megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen" + }, + "title": "Az AirVisual konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/airvisual/translations/sensor.cs.json b/homeassistant/components/airvisual/translations/sensor.cs.json new file mode 100644 index 00000000000..44c834c7df6 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.cs.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Oxid uhelnat\u00fd", + "n2": "Oxid dusi\u010dit\u00fd", + "o3": "Oz\u00f3n", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Oxid si\u0159i\u010dit\u00fd" + }, + "airvisual__pollutant_level": { + "good": "Dobr\u00e9", + "hazardous": "Riskantn\u00ed", + "moderate": "M\u00edrn\u00e9", + "unhealthy": "Nezdrav\u00e9", + "unhealthy_sensitive": "Nezdrav\u00e9 pro citliv\u00e9 skupiny", + "very_unhealthy": "Velmi nezdrav\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.es.json b/homeassistant/components/airvisual/translations/sensor.es.json new file mode 100644 index 00000000000..4a8a7cea1e3 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.es.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitr\u00f3geno", + "o3": "Ozono", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Di\u00f3xido de azufre" + }, + "airvisual__pollutant_level": { + "good": "Bien", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_sensitive": "Incorrecto para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.hu.json b/homeassistant/components/airvisual/translations/sensor.hu.json new file mode 100644 index 00000000000..93fbb2ce510 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.hu.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Sz\u00e9n-monoxid", + "n2": "Nitrog\u00e9n-dioxid", + "o3": "\u00d3zon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "K\u00e9n-dioxid" + }, + "airvisual__pollutant_level": { + "good": "J\u00f3", + "hazardous": "Vesz\u00e9lyes", + "moderate": "M\u00e9rs\u00e9kelt", + "unhealthy": "Eg\u00e9szs\u00e9gtelen", + "unhealthy_sensitive": "Eg\u00e9szs\u00e9gtelen az \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra", + "very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.no.json b/homeassistant/components/airvisual/translations/sensor.no.json index 86c95f8e8f2..cf142ad9f1a 100644 --- a/homeassistant/components/airvisual/translations/sensor.no.json +++ b/homeassistant/components/airvisual/translations/sensor.no.json @@ -1,8 +1,20 @@ { "state": { "airvisual__pollutant_label": { + "co": "Karbonmonoksid", + "n2": "Nitrogendioksid", + "o3": "Ozon", "p1": "PM10", - "p2": "PM2.5" + "p2": "PM2.5", + "s2": "Svoveldioksid" + }, + "airvisual__pollutant_level": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "Moderat", + "unhealthy": "Usunt", + "unhealthy_sensitive": "Usunt for sensitive grupper", + "very_unhealthy": "Veldig usunt" } } } \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 85f89f3043b..5cebe3622dc 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( diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index 6d8ee9c08c3..bbcb26f7184 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -12,6 +12,7 @@ "is_armed_away": "{entity_name} est arm\u00e9", "is_armed_home": "{entity_name} est arm\u00e9 \u00e0 la maison", "is_armed_night": "{entity_name} est arm\u00e9 la nuit", + "is_armed_vacation": "{entity_name} est arm\u00e9 en mode vacances", "is_disarmed": "{entity_name} est d\u00e9sarm\u00e9", "is_triggered": "{entity_name} est d\u00e9clench\u00e9" }, diff --git a/homeassistant/components/alarm_control_panel/translations/hu.json b/homeassistant/components/alarm_control_panel/translations/hu.json index 961006938d9..5eba25a9ec2 100644 --- a/homeassistant/components/alarm_control_panel/translations/hu.json +++ b/homeassistant/components/alarm_control_panel/translations/hu.json @@ -9,7 +9,12 @@ "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" }, "condition_type": { - "is_armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edtve" + "is_armed_away": "{entity_name} \u00e9les\u00edtve van", + "is_armed_home": "{entity_name} \u00e9les\u00edtett otthoni m\u00f3dban", + "is_armed_night": "{entity_name} \u00e9les\u00edtett \u00e9jszaka m\u00f3dban", + "is_armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edtve", + "is_disarmed": "{entity_name} hat\u00e1stalan\u00edtva", + "is_triggered": "{entity_name} aktiv\u00e1lva van" }, "trigger_type": { "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", diff --git a/homeassistant/components/alarm_control_panel/translations/no.json b/homeassistant/components/alarm_control_panel/translations/no.json index 465dd250086..ad8ed2c9c74 100644 --- a/homeassistant/components/alarm_control_panel/translations/no.json +++ b/homeassistant/components/alarm_control_panel/translations/no.json @@ -4,6 +4,7 @@ "arm_away": "Aktiver {entity_name} borte", "arm_home": "Aktiver {entity_name} hjemme", "arm_night": "Aktiver {entity_name} natt", + "arm_vacation": "{entity_name} ferie", "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} er aktivert borte", "is_armed_home": "{entity_name} er aktivert hjemme", "is_armed_night": "{entity_name} er aktivert natt", + "is_armed_vacation": "{entity_name} er armert ferie", "is_disarmed": "{entity_name} er deaktivert", "is_triggered": "{entity_name} er utl\u00f8st" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} aktivert borte", "armed_home": "{entity_name} aktivert hjemme", "armed_night": "{entity_name} aktivert natt", + "armed_vacation": "{entity_name} armert ferie", "disarmed": "{entity_name} deaktivert", "triggered": "{entity_name} utl\u00f8st" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Armert tilpasset unntak", "armed_home": "Armert hjemme", "armed_night": "Armert natt", + "armed_vacation": "Armert ferie", "arming": "Armerer", "disarmed": "Avsl\u00e5tt", "disarming": "Disarmer", 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() diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 67b7ee4861a..16471010ee9 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -32,6 +32,6 @@ class AlarmDecoderSensor(SensorEntity): ) def _message_callback(self, message): - if self._attr_state != message.text: - self._attr_state = message.text + if self._attr_native_value != message.text: + self._attr_native_value = message.text self.schedule_update_ha_state() diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json index 47db325f06c..ace9c7059ca 100644 --- a/homeassistant/components/alarmdecoder/translations/hu.json +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -31,6 +31,7 @@ "error": { "int": "Az al\u00e1bbi mez\u0151nek eg\u00e9sz sz\u00e1mnak kell lennie.", "loop_range": "Az RF hurok eg\u00e9sz sz\u00e1m\u00e1nak 1 \u00e9s 4 k\u00f6z\u00f6tt kell lennie.", + "loop_rfid": "Az RF hurok nem haszn\u00e1lhat\u00f3 RF sorozat n\u00e9lk\u00fcl.", "relay_inclusive": "A rel\u00e9c\u00edm \u00e9s a rel\u00e9csatorna egym\u00e1st\u00f3l f\u00fcgg, \u00e9s egy\u00fctt kell felt\u00fcntetni." }, "step": { @@ -55,6 +56,7 @@ "zone_name": "Z\u00f3na neve", "zone_relayaddr": "Rel\u00e9 c\u00edm", "zone_relaychan": "Rel\u00e9 csatorna", + "zone_rfid": "RF soros", "zone_type": "Z\u00f3na t\u00edpusa" }, "description": "Adja meg a {zone_number} z\u00f3na adatait. {zone_number} z\u00f3na t\u00f6rl\u00e9s\u00e9hez hagyja \u00fcresen a Z\u00f3na neve elemet.", diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index db1fa990c54..fcd6ebf6ae2 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -99,7 +99,7 @@ class AlexaCapability: return False @staticmethod - def properties_non_controllable() -> bool: + def properties_non_controllable() -> bool | None: """Return True if non controllable.""" return None diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 29643bacc53..a6adc488f75 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -1,4 +1,6 @@ """Alexa related errors.""" +from __future__ import annotations + from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS @@ -22,8 +24,8 @@ class AlexaError(Exception): A handler can raise subclasses of this to return an error to the request. """ - namespace = None - error_type = None + namespace: str | None = None + error_type: str | None = None def __init__(self, error_message, payload=None): """Initialize an alexa error.""" diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 512de247ff2..583485ca703 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -112,7 +112,7 @@ class AlphaVantageSensor(SensorEntity): self._symbol = symbol[CONF_SYMBOL] self._attr_name = symbol.get(CONF_NAME, self._symbol) self._timeseries = timeseries - self._attr_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) + self._attr_native_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) self._attr_icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) def update(self): @@ -120,7 +120,7 @@ class AlphaVantageSensor(SensorEntity): _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) values = next(iter(all_values.values())) - self._attr_state = values["1. open"] + self._attr_native_value = values["1. open"] self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -148,7 +148,7 @@ class AlphaVantageForeignExchange(SensorEntity): else f"{self._to_currency}/{self._from_currency}" ) self._attr_icon = ICONS.get(self._from_currency, "USD") - self._attr_unit_of_measurement = self._to_currency + self._attr_native_unit_of_measurement = self._to_currency def update(self): """Get the latest data and updates the states.""" @@ -160,7 +160,7 @@ class AlphaVantageForeignExchange(SensorEntity): values, _ = self._foreign_exchange.get_currency_exchange_rate( from_currency=self._from_currency, to_currency=self._to_currency ) - self._attr_state = round(float(values["5. Exchange Rate"]), 4) + self._attr_native_value = round(float(values["5. Exchange Rate"]), 4) self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index d2570bea710..3fd57c17c63 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -39,38 +39,38 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { SensorEntityDescription( key="particulate_matter_2_5", name="Particulate Matter < 2.5 μm", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", name="Particulate Matter < 10 μm", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", name="Sulphur Dioxide (SO2)", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", name="Nitrogen Dioxide (NO2)", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="ozone", name="Ozone", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="carbon_monoxide", name="Carbon Monoxide (CO)", device_class=DEVICE_CLASS_CO, - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( @@ -85,21 +85,21 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Grass Pollen", icon="mdi:grass", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", name="Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", name="Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="grass_risk", @@ -124,7 +124,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Poaceae Grass Pollen", icon="mdi:grass", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -132,7 +132,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Alder Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -140,7 +140,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Birch Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -148,7 +148,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Cypress Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -156,7 +156,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Elm Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -164,7 +164,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Hazel Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -172,7 +172,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Oak Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -180,7 +180,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Pine Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -188,7 +188,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Plane Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -196,7 +196,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Poplar Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -204,7 +204,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Chenopod Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -212,7 +212,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Mugwort Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -220,7 +220,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Nettle Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -228,7 +228,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Ragweed Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), ], diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index ecd04ffd204..bd125ac973e 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -66,7 +66,7 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" value = getattr(self.coordinator.data, self.entity_description.key) if isinstance(value, str): 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") diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 04035f04cca..3898535c427 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "access_token": "Ismeretlen hiba a hozz\u00e1f\u00e9r\u00e9si token gener\u00e1l\u00e1s\u00e1ban.", "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "error": { + "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "no_token": "Nem hiteles\u00edtett Ambiclimate" + }, + "step": { + "auth": { + "description": "K\u00e9rj\u00fck, k\u00f6vesse ezt a [link] ({authorization_url} Author_url}) \u00e9s ** Enged\u00e9lyezze ** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot.\n (Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "title": "Ambiclimate hiteles\u00edt\u00e9se" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 42b22d26a10..35b4770e872 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.2.5"], + "requirements": ["aioambient==1.2.6"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a606b401bc0..935a53e9384 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -61,7 +61,7 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ambient, mac_address, station_name, sensor_type, sensor_name, device_class ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def update_from_latest_data(self) -> None: @@ -75,10 +75,10 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ].get(TYPE_SOLARRADIATION) if w_m2_brightness_val is None: - self._attr_state = None + self._attr_native_value = None else: - self._attr_state = round(float(w_m2_brightness_val) / 0.0079) + self._attr_native_value = round(float(w_m2_brightness_val) / 0.0079) else: - self._attr_state = self._ambient.stations[self._mac_address][ + self._attr_native_value = self._ambient.stations[self._mac_address][ ATTR_LAST_DATA ].get(self._sensor_type) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 0add382b81f..98e0be73ef4 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -53,7 +53,7 @@ _CROSSLINE_DETECTED_PARAMS = ( DEVICE_CLASS_MOTION, "CrossLineDetection", ) -BINARY_SENSORS = { +RAW_BINARY_SENSORS = { BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, @@ -64,7 +64,7 @@ BINARY_SENSORS = { } BINARY_SENSORS = { k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) - for k, v in BINARY_SENSORS.items() + for k, v in RAW_BINARY_SENSORS.items() } _EXCLUSIVE_OPTIONS = [ {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 5c7f8acf94a..1478c658d18 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,4 +1,6 @@ """Support for Amcrest IP cameras.""" +from __future__ import annotations + import asyncio from datetime import timedelta from functools import partial @@ -181,7 +183,9 @@ class AmcrestCam(Camera): finally: self._snapshot_task = None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" _LOGGER.debug("Take snapshot from %s", self._name) try: diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index a30de62494e..de8370a15fc 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -61,7 +61,7 @@ class AmcrestSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -76,7 +76,7 @@ class AmcrestSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index d41970a79de..944acc6ef9d 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -5,12 +5,13 @@ from homeassistant.components import websocket_api from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.typing import ConfigType from .analytics import Analytics from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA -async def async_setup(hass: HomeAssistant, _): +async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: """Set up the analytics integration.""" analytics = Analytics(hass) diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index adedb297cd1..4bef3848617 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -50,12 +50,12 @@ class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index d1e379435a0..00be4fa50c4 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.3.4", + "adb-shell[async]==0.4.0", "androidtv[async]==0.0.60", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 08ae2999e37..8bc53bd86b7 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: diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index bf1b8bf6db5..5937ff6a852 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -165,16 +165,16 @@ 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] - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][3] def update(self): """Get the latest status and use it to update our sensor state.""" if self.type.upper() not in self._data.status: - self._attr_state = None + self._attr_native_value = None else: - self._attr_state, inferred_unit = infer_unit( + self._attr_native_value, inferred_unit = infer_unit( self._data.status[self.type.upper()] ) - if not self._attr_unit_of_measurement: - self._attr_unit_of_measurement = inferred_unit + if not self._attr_native_unit_of_measurement: + self._attr_native_unit_of_measurement = inferred_unit diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 0a11cf04651..a91d8540286 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -43,7 +43,6 @@ from homeassistant.helpers.system_info import async_get_system_info _LOGGER = logging.getLogger(__name__) ATTR_BASE_URL = "base_url" -ATTR_CURRENCY = "currency" ATTR_EXTERNAL_URL = "external_url" ATTR_INTERNAL_URL = "internal_url" ATTR_LOCATION_NAME = "location_name" @@ -196,7 +195,6 @@ class APIDiscoveryView(HomeAssistantView): # always needs authentication ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, - ATTR_CURRENCY: None, } with suppress(NoURLAvailableError): diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 01f31757c9d..394f8844adb 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -92,10 +92,11 @@ class AquaLogicSensor(SensorEntity): panel = self._processor.panel 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() + self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][0] else: - self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + + self._attr_native_value = getattr(panel, self._type) + self.async_write_ha_state() else: - self._attr_unit_of_measurement = None + self._attr_native_unit_of_measurement = None diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index c1df4fc0587..d28de3b92aa 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -36,7 +36,7 @@ async def _await_cancel(task): await task -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the component.""" hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_TASKS] = {} diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index e1784c4ad66..9539ad39bed 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -5,14 +5,27 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "error": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "flow_title": "{host}", "step": { + "confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni az Arcam FMJ \"{host}\" eszk\u00f6zt a HomeAssistanthoz?" + }, "user": { "data": { "host": "Hoszt", "port": "Port" - } + }, + "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t." } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} bekapcsol\u00e1s\u00e1t k\u00e9rt\u00e9k" + } } } \ No newline at end of file diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index fa624a7d167..0853fb5537d 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -42,4 +42,4 @@ class ArduinoSensor(SensorEntity): def update(self): """Get the latest value from the pin.""" - self._attr_state = self._board.get_analog_inputs()[self._pin][1] + self._attr_native_value = self._board.get_analog_inputs()[self._pin][1] diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 7129b989f47..addd666e30e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -141,7 +141,7 @@ class ArestSensor(SensorEntity): self.arest = arest self._attr_name = f"{location.title()} {name.title()}" self._variable = variable - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._renderer = renderer if pin is not None: @@ -155,9 +155,9 @@ class ArestSensor(SensorEntity): self._attr_available = self.arest.available values = self.arest.data if "error" in values: - self._attr_state = values["error"] + self._attr_native_value = values["error"] else: - self._attr_state = self._renderer( + self._attr_native_value = self._renderer( values.get("value", values.get(self._variable, None)) ) diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 87c6216e56d..6b14f0cee0c 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -1,4 +1,6 @@ """Support for Netgear Arlo IP cameras.""" +from __future__ import annotations + import logging from haffmpeg.camera import CameraMjpeg @@ -62,7 +64,9 @@ class ArloCam(Camera): self._last_refresh = None self.attrs = {} - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return self._camera.last_image_from_cache diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index c794bf1ef5e..cc08cd133e4 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -1,13 +1,21 @@ """Sensor support for Netgear Arlo IP cameras.""" +from __future__ import annotations + +from dataclasses import replace import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -22,22 +30,59 @@ from . import ATTRIBUTION, DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = { - "last_capture": ["Last", None, "run-fast"], - "total_cameras": ["Arlo Cameras", None, "video"], - "captured_today": ["Captured Today", None, "file-video"], - "battery_level": ["Battery Level", PERCENTAGE, "battery-50"], - "signal_strength": ["Signal Strength", None, "signal"], - "temperature": ["Temperature", TEMP_CELSIUS, "thermometer"], - "humidity": ["Humidity", PERCENTAGE, "water-percent"], - "air_quality": ["Air Quality", CONCENTRATION_PARTS_PER_MILLION, "biohazard"], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="last_capture", + name="Last", + icon="mdi:run-fast", + ), + SensorEntityDescription( + key="total_cameras", + name="Arlo Cameras", + icon="mdi:video", + ), + SensorEntityDescription( + key="captured_today", + name="Captured Today", + icon="mdi:file-video", + ), + SensorEntityDescription( + key="battery_level", + name="Battery Level", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + SensorEntityDescription( + key="signal_strength", + name="Signal Strength", + icon="mdi:signal", + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + ), + SensorEntityDescription( + key="air_quality", + name="Air Quality", + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + icon="mdi:biohazard", + ), +) + +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ) } ) @@ -50,24 +95,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return sensors = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if sensor_type == "total_cameras": - sensors.append(ArloSensor(SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) + for sensor_original in SENSOR_TYPES: + if sensor_original.key not in config[CONF_MONITORED_CONDITIONS]: + continue + sensor_entry = replace(sensor_original) + if sensor_entry.key == "total_cameras": + sensors.append(ArloSensor(arlo, sensor_entry)) else: for camera in arlo.cameras: - if sensor_type in ("temperature", "humidity", "air_quality"): + if sensor_entry.key in ("temperature", "humidity", "air_quality"): continue - name = f"{SENSOR_TYPES[sensor_type][0]} {camera.name}" - sensors.append(ArloSensor(name, camera, sensor_type)) + sensor_entry.name = f"{sensor_entry.name} {camera.name}" + sensors.append(ArloSensor(camera, sensor_entry)) for base_station in arlo.base_stations: if ( - sensor_type in ("temperature", "humidity", "air_quality") + sensor_entry.key in ("temperature", "humidity", "air_quality") and base_station.model_id == "ABC1000" ): - name = f"{SENSOR_TYPES[sensor_type][0]} {base_station.name}" - sensors.append(ArloSensor(name, base_station, sensor_type)) + sensor_entry.name = f"{sensor_entry.name} {base_station.name}" + sensors.append(ArloSensor(base_station, sensor_entry)) add_entities(sensors, True) @@ -75,19 +123,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArloSensor(SensorEntity): """An implementation of a Netgear Arlo IP sensor.""" - def __init__(self, name, device, sensor_type): + def __init__(self, device, sensor_entry): """Initialize an Arlo sensor.""" - _LOGGER.debug("ArloSensor created for %s", name) - self._name = name + self.entity_description = sensor_entry self._data = device - self._sensor_type = sensor_type self._state = None - self._icon = f"mdi:{SENSOR_TYPES.get(self._sensor_type)[2]}" - - @property - def name(self): - """Return the name of this camera.""" - return self._name async def async_added_to_hass(self): """Register callbacks.""" @@ -103,43 +143,29 @@ class ArloSensor(SensorEntity): self.async_schedule_update_ha_state(True) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery_level" and self._state is not None: + if self.entity_description.key == "battery_level" and self._state is not None: return icon_for_battery_level( battery_level=int(self._state), charging=False ) - return self._icon - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] - - @property - def device_class(self): - """Return the device class of the sensor.""" - if self._sensor_type == "temperature": - return DEVICE_CLASS_TEMPERATURE - if self._sensor_type == "humidity": - return DEVICE_CLASS_HUMIDITY - return None + return self.entity_description.icon def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating Arlo sensor %s", self.name) - if self._sensor_type == "total_cameras": + if self.entity_description.key == "total_cameras": self._state = len(self._data.cameras) - elif self._sensor_type == "captured_today": + elif self.entity_description.key == "captured_today": self._state = len(self._data.captured_today) - elif self._sensor_type == "last_capture": + elif self.entity_description.key == "last_capture": try: video = self._data.last_video self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") @@ -151,31 +177,31 @@ class ArloSensor(SensorEntity): _LOGGER.debug(error_msg) self._state = None - elif self._sensor_type == "battery_level": + elif self.entity_description.key == "battery_level": try: self._state = self._data.battery_level except TypeError: self._state = None - elif self._sensor_type == "signal_strength": + elif self.entity_description.key == "signal_strength": try: self._state = self._data.signal_strength except TypeError: self._state = None - elif self._sensor_type == "temperature": + elif self.entity_description.key == "temperature": try: self._state = self._data.ambient_temperature except TypeError: self._state = None - elif self._sensor_type == "humidity": + elif self.entity_description.key == "humidity": try: self._state = self._data.ambient_humidity except TypeError: self._state = None - elif self._sensor_type == "air_quality": + elif self.entity_description.key == "air_quality": try: self._state = self._data.ambient_air_quality except TypeError: @@ -189,7 +215,7 @@ class ArloSensor(SensorEntity): attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs["brand"] = DEFAULT_BRAND - if self._sensor_type != "total_cameras": + if self.entity_description.key != "total_cameras": attrs["model"] = self._data.model_id return attrs diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 2300319f9a4..321be5035cd 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -138,7 +138,7 @@ class ArwnSensor(SensorEntity): # This mqtt topic for the sensor which is its uid self._attr_unique_id = topic self._state_key = state_key - self._attr_unit_of_measurement = units + self._attr_native_unit_of_measurement = units self._attr_icon = icon self._attr_device_class = device_class @@ -147,5 +147,5 @@ class ArwnSensor(SensorEntity): ev = {} ev.update(event) self._attr_extra_state_attributes = ev - self._attr_state = ev.get(self._state_key, None) + self._attr_native_value = ev.get(self._state_key, None) self.async_write_ha_state() diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 0b5d81e3de9..3e954eb25b9 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -60,6 +60,12 @@ class AsusWrtDevice(ScannerEntity): self._device = device self._attr_unique_id = device.mac self._attr_name = device.name or DEFAULT_DEVICE_NAME + self._attr_device_info = { + "connections": {(CONNECTION_NETWORK_MAC, device.mac)}, + "default_model": "ASUSWRT Tracked device", + } + if device.name: + self._attr_device_info["default_name"] = device.name @property def is_connected(self): @@ -90,11 +96,6 @@ class AsusWrtDevice(ScannerEntity): def async_on_demand_update(self): """Update state.""" self._device = self._router.devices[self._device.mac] - self._attr_device_info = { - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, - } - if self._device.name: - self._attr_device_info["default_name"] = self._device.name self._attr_extra_state_attributes = {} if self._device.last_activity: self._attr_extra_state_attributes[ diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 679ae832394..a9a005b9837 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,15 +1,19 @@ """Asuswrt status sensors.""" from __future__ import annotations +from dataclasses import dataclass import logging from numbers import Number -from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,62 +29,90 @@ from .const import ( ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter + +@dataclass +class AsusWrtSensorEntityDescription(SensorEntityDescription): + """A class that describes AsusWrt sensor entities.""" + + factor: int | None = None + precision: int = 2 + + DEFAULT_PREFIX = "Asuswrt" - -SENSOR_DEVICE_CLASS = "device_class" -SENSOR_ICON = "icon" -SENSOR_NAME = "name" -SENSOR_UNIT = "unit" -SENSOR_FACTOR = "factor" -SENSOR_DEFAULT_ENABLED = "default_enabled" - UNIT_DEVICES = "Devices" -CONNECTION_SENSORS = { - SENSORS_CONNECTED_DEVICE[0]: { - SENSOR_NAME: "Devices Connected", - SENSOR_UNIT: UNIT_DEVICES, - SENSOR_FACTOR: 0, - SENSOR_ICON: "mdi:router-network", - SENSOR_DEFAULT_ENABLED: True, - }, - SENSORS_RATES[0]: { - SENSOR_NAME: "Download Speed", - SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, - SENSOR_FACTOR: 125000, - SENSOR_ICON: "mdi:download-network", - }, - SENSORS_RATES[1]: { - SENSOR_NAME: "Upload Speed", - SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, - SENSOR_FACTOR: 125000, - SENSOR_ICON: "mdi:upload-network", - }, - SENSORS_BYTES[0]: { - SENSOR_NAME: "Download", - SENSOR_UNIT: DATA_GIGABYTES, - SENSOR_FACTOR: 1000000000, - SENSOR_ICON: "mdi:download", - }, - SENSORS_BYTES[1]: { - SENSOR_NAME: "Upload", - SENSOR_UNIT: DATA_GIGABYTES, - SENSOR_FACTOR: 1000000000, - SENSOR_ICON: "mdi:upload", - }, - SENSORS_LOAD_AVG[0]: { - SENSOR_NAME: "Load Avg (1m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, - SENSORS_LOAD_AVG[1]: { - SENSOR_NAME: "Load Avg (5m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, - SENSORS_LOAD_AVG[2]: { - SENSOR_NAME: "Load Avg (15m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, -} +CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( + AsusWrtSensorEntityDescription( + key=SENSORS_CONNECTED_DEVICE[0], + name="Devices Connected", + icon="mdi:router-network", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=UNIT_DEVICES, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_RATES[0], + name="Download Speed", + icon="mdi:download-network", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + entity_registry_enabled_default=False, + factor=125000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_RATES[1], + name="Upload Speed", + icon="mdi:upload-network", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + entity_registry_enabled_default=False, + factor=125000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_BYTES[0], + name="Download", + icon="mdi:download", + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement=DATA_GIGABYTES, + entity_registry_enabled_default=False, + factor=1000000000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_BYTES[1], + name="Upload", + icon="mdi:upload", + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement=DATA_GIGABYTES, + entity_registry_enabled_default=False, + factor=1000000000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[0], + name="Load Avg (1m)", + icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[1], + name="Load Avg (5m)", + icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[2], + name="Load Avg (15m)", + icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), +) _LOGGER = logging.getLogger(__name__) @@ -95,13 +127,13 @@ async def async_setup_entry( for sensor_data in router.sensors_coordinator.values(): coordinator = sensor_data[KEY_COORDINATOR] sensors = sensor_data[KEY_SENSORS] - for sensor_key in sensors: - if sensor_key in CONNECTION_SENSORS: - entities.append( - AsusWrtSensor( - coordinator, router, sensor_key, CONNECTION_SENSORS[sensor_key] - ) - ) + entities.extend( + [ + AsusWrtSensor(coordinator, router, sensor_descr) + for sensor_descr in CONNECTION_SENSORS + if sensor_descr.key in sensors + ] + ) async_add_entities(entities, True) @@ -113,39 +145,23 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): self, coordinator: DataUpdateCoordinator, router: AsusWrtRouter, - sensor_type: str, - sensor_def: dict[str, Any], + description: AsusWrtSensorEntityDescription, ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) - self._router = router - self._sensor_type = sensor_type - self._attr_name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}" - self._factor = sensor_def.get(SENSOR_FACTOR) + self.entity_description = description + + self._attr_name = f"{DEFAULT_PREFIX} {description.name}" self._attr_unique_id = f"{DOMAIN} {self.name}" - self._attr_entity_registry_enabled_default = sensor_def.get( - SENSOR_DEFAULT_ENABLED, False - ) - self._attr_unit_of_measurement = sensor_def.get(SENSOR_UNIT) - self._attr_icon = sensor_def.get(SENSOR_ICON) - self._attr_device_class = sensor_def.get(SENSOR_DEVICE_CLASS) + self._attr_device_info = router.device_info + self._attr_extra_state_attributes = {"hostname": router.host} @property - def state(self) -> str: + def native_value(self) -> str | None: """Return current state.""" - state = self.coordinator.data.get(self._sensor_type) - if state is None: - return None - if self._factor and isinstance(state, Number): - return round(state / self._factor, 2) + descr = self.entity_description + state = self.coordinator.data.get(descr.key) + if state is not None: + if descr.factor and isinstance(state, Number): + return round(state / descr.factor, descr.precision) return state - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the attributes.""" - return {"hostname": self._router.host} - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return self._router.device_info diff --git a/homeassistant/components/asuswrt/translations/zh-Hans.json b/homeassistant/components/asuswrt/translations/zh-Hans.json new file mode 100644 index 00000000000..69f7bf98df3 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/zh-Hans.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_host": "\u65e0\u6548\u7684\u4e3b\u673a\u5730\u5740\u6216 IP \u5730\u5740", + "pwd_and_ssh": "\u53ea\u63d0\u4f9b\u5bc6\u7801\u6216 SSH \u5bc6\u94a5\u6587\u4ef6", + "pwd_or_ssh": "\u8bf7\u63d0\u4f9b\u5bc6\u7801\u6216 SSH \u5bc6\u94a5\u6587\u4ef6", + "ssh_not_file": "\u672a\u627e\u5230 SSH \u5bc6\u94a5\u6587\u4ef6", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "mode": "\u4f7f\u7528\u6a21\u5f0f", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "protocol": "\u901a\u4fe1\u534f\u8bae", + "ssh_key": "SSH \u5bc6\u94a5\u6587\u4ef6\u8def\u5f84 (\u4e0d\u662f\u5bc6\u7801)", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8bbe\u7f6e\u8fde\u63a5\u5230\u8def\u7531\u5668\u6240\u9700\u7684\u53c2\u6570", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u7b49\u5f85\u591a\u5c11\u79d2\u540e\u5219\u5224\u5b9a\u8bbe\u5907\u79bb\u5f00", + "dnsmasq": "\u8def\u7531\u5668\u4e2d\u7684 dnsmasq.leases \u6587\u4ef6\u4f4d\u7f6e", + "interface": "\u60f3\u8981\u76d1\u6d4b\u7684\u7aef\u53e3(\u4f8b\u5982: eth0,eth1 \u7b49)", + "require_ip": "\u8bbe\u5907\u5fc5\u987b\u5177\u6709 IP (\u7528\u4e8e\u63a5\u5165\u70b9\u6a21\u5f0f)" + }, + "title": "AsusWRT \u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 014c6cb463e..386b5999712 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -49,10 +49,12 @@ class AtagSensor(AtagEntity, SensorEntity): PERCENTAGE, TIME_HOURS, ): - self._attr_unit_of_measurement = coordinator.data.report[self._id].measure + self._attr_native_unit_of_measurement = coordinator.data.report[ + self._id + ].measure @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data.report[self._id].state diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 134f3bedfe8..8c3b4a055b0 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -4,14 +4,16 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unauthorized": "A p\u00e1ros\u00edt\u00e1s megtagadva, ellen\u0151rizze az eszk\u00f6z hiteles\u00edt\u00e9si k\u00e9r\u00e9s\u00e9t" }, "step": { "user": { "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" } } } diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index bcb7b4f1ece..59d193ec8e2 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -8,12 +8,14 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, @@ -219,8 +221,6 @@ 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 @@ -229,10 +229,13 @@ class AtomeSensor(SensorEntity): self._sensor_type = sensor_type if sensor_type == LIVE_TYPE: - self._attr_unit_of_measurement = POWER_WATT + self._attr_device_class = DEVICE_CLASS_POWER + self._attr_native_unit_of_measurement = POWER_WATT self._attr_state_class = STATE_CLASS_MEASUREMENT else: - self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING def update(self): """Update device state.""" @@ -240,13 +243,13 @@ class AtomeSensor(SensorEntity): update_function() if self._sensor_type == LIVE_TYPE: - self._attr_state = self._data.live_power + self._attr_native_value = self._data.live_power self._attr_extra_state_attributes = { "subscribed_power": self._data.subscribed_power, "is_connected": self._data.is_connected, } else: - self._attr_state = getattr(self._data, f"{self._sensor_type}_usage") + self._attr_native_value = getattr(self._data, f"{self._sensor_type}_usage") self._attr_extra_state_attributes = { "price": getattr(self._data, f"{self._sensor_type}_price") } diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 6bb47a06eee..6f9ecf1b182 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -1,4 +1,5 @@ """Support for August doorbell camera.""" +from __future__ import annotations from yalexs.activity import ActivityType from yalexs.util import update_doorbell_image_from_activity @@ -68,7 +69,9 @@ class AugustCamera(AugustEntityMixin, Camera): if doorbell_activity is not None: update_doorbell_image_from_activity(self._detail, doorbell_activity) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" self._update_from_data() diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 74caa4b4a78..fc365102926 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -13,6 +13,10 @@ "hostname": "connect", "macaddress": "B8B7F1*" }, + { + "hostname": "connect", + "macaddress": "2C9FFB*" + }, { "hostname": "august*", "macaddress": "E076D0*" diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index a174964f349..b6d93d3b3b1 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -146,7 +146,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._attr_available = True if lock_activity is not None: - self._attr_state = lock_activity.operated_by + self._attr_native_value = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad self._operated_autorelock = lock_activity.operated_autorelock @@ -208,7 +208,7 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity): """Representation of an August sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, data, sensor_type, device, old_device): """Initialize the sensor.""" @@ -223,8 +223,8 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity): def _update_from_data(self): """Get the latest state of the sensor.""" state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] - self._attr_state = state_provider(self._detail) - self._attr_available = self._attr_state is not None + self._attr_native_value = state_provider(self._detail) + self._attr_available = self._attr_native_value is not None @property def old_unique_id(self) -> str: diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index fec6ad93b26..aeaef514e71 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -30,6 +30,7 @@ "data": { "code": "Ellen\u0151rz\u0151 k\u00f3d" }, + "description": "K\u00e9rj\u00fck, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } diff --git a/homeassistant/components/august/translations/zh-Hans.json b/homeassistant/components/august/translations/zh-Hans.json new file mode 100644 index 00000000000..b932dae2511 --- /dev/null +++ b/homeassistant/components/august/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_validate": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user_validate": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 76be6ca97f8..96bdbbf1370 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -22,9 +22,9 @@ async def async_setup_entry(hass, entry, async_add_entries): class AuroraSensor(AuroraEntity, SensorEntity): """Implementation of an aurora sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property - def state(self): + def native_value(self): """Return % chance the aurora is visible.""" return self.coordinator.data diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 9c798b8e6d4..b1bcec18796 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -51,7 +51,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): """Representation of a Sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_device_class = DEVICE_CLASS_POWER def __init__(self, client, name, typename): @@ -68,7 +68,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): self.client.connect() # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) - self._attr_state = round(power_watts, 1) + self._attr_native_value = round(power_watts, 1) except AuroraError as error: # aurorapy does not have different exceptions (yet) for dealing # with timeout vs other comms errors. @@ -82,7 +82,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): _LOGGER.debug("No response from inverter (could be dark)") else: raise error - self._attr_state = None + self._attr_native_value = None finally: if self.client.serline.isOpen(): self.client.close() diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 968587c3b10..3b46d3b2317 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -144,7 +144,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return False @property - def state(self) -> float: + def native_value(self) -> float: """Return the state, rounding off to reasonable values.""" state: float @@ -175,7 +175,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS] @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" return SENSOR_TYPES[self._kind][ATTR_UNIT] diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 53827adf344..f465186a95b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -21,7 +21,8 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" - } + }, + "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 972690ede97..709de5851ad 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "link_local_address": "A linkek helyi c\u00edmei nem t\u00e1mogatottak", + "not_axis_device": "A felfedezett eszk\u00f6z nem Axis eszk\u00f6z" }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", @@ -17,7 +19,8 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "\u00c1ll\u00edtsa be az Axis eszk\u00f6zt" } } }, @@ -26,7 +29,8 @@ "configure_stream": { "data": { "stream_profile": "V\u00e1lassza ki a haszn\u00e1lni k\u00edv\u00e1nt adatfolyam-profilt" - } + }, + "title": "Axis eszk\u00f6z vide\u00f3 stream opci\u00f3k" } } } diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index d7589cf5014..67d472abc1e 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -71,7 +71,7 @@ class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): unit_of_measurement: str = "", ) -> None: """Initialize Azure DevOps sensor.""" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self.client = client self.organization = organization self.project = project @@ -107,7 +107,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): _LOGGER.warning(exception) self._attr_available = False return False - self._attr_state = build.build_number + self._attr_native_value = build.build_number self._attr_extra_state_attributes = { "definition_id": build.definition.id, "definition_name": build.definition.name, diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index f85c6795fd5..e42ebc8d8e2 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -6,14 +6,25 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "project_error": "Nem siker\u00fclt lek\u00e9rni a projekt adatait." }, "flow_title": "{project_url}", "step": { "reauth": { - "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." + "data": { + "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" + }, + "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { + "data": { + "organization": "Szervezet", + "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)", + "project": "Projekt" + }, + "description": "\u00c1ll\u00edtson be egy Azure DevOps-p\u00e9ld\u00e1nyt a projekt el\u00e9r\u00e9s\u00e9hez. Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token csak mag\u00e1nprojekthez sz\u00fcks\u00e9ges.", "title": "Azure DevOps Project hozz\u00e1ad\u00e1sa" } } diff --git a/homeassistant/components/azure_devops/translations/zh-Hans.json b/homeassistant/components/azure_devops/translations/zh-Hans.json index b0c629646e2..d6a6e62e27c 100644 --- a/homeassistant/components/azure_devops/translations/zh-Hans.json +++ b/homeassistant/components/azure_devops/translations/zh-Hans.json @@ -1,8 +1,32 @@ { "config": { + "abort": { + "already_configured": "\u8d26\u6237\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" + "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548", + "project_error": "\u65e0\u6cd5\u83b7\u53d6\u9879\u76ee\u4fe1\u606f\u3002" + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c (PAT)" + }, + "description": "{project_url} \u8eab\u4efd\u9a8c\u8bc1\u5931\u8d25\u3002\u8bf7\u8f93\u5165\u60a8\u5f53\u524d\u7684\u51ed\u636e\u3002", + "title": "\u91cd\u9a8c\u8bc1" + }, + "user": { + "data": { + "organization": "\u7ec4\u7ec7", + "personal_access_token": "\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c (PAT)", + "project": "\u9879\u76ee" + }, + "description": "\u8bbe\u7f6e Azure DevOps \u5b9e\u4f8b\u4ee5\u8bbf\u95ee\u60a8\u7684\u9879\u76ee\u3002\u79c1\u4eba\u9879\u76ee\u624d\u9700\u8981\u63d0\u4f9b\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c\u3002", + "title": "\u6dfb\u52a0 Azure DevOps \u9879\u76ee" + } } } } \ No newline at end of file diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index b0ace5fa675..9ccd197e05e 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -94,7 +94,7 @@ class BboxUptimeSensor(SensorEntity): def __init__(self, bbox_data, sensor_type, name): """Initialize the sensor.""" self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data @@ -104,7 +104,7 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_state = uptime.replace(microsecond=0).isoformat() + self._attr_native_value = uptime.replace(microsecond=0).isoformat() class BboxSensor(SensorEntity): @@ -116,7 +116,7 @@ class BboxSensor(SensorEntity): """Initialize the sensor.""" self.type = sensor_type self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data @@ -124,19 +124,25 @@ class BboxSensor(SensorEntity): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() if self.type == "down_max_bandwidth": - self._attr_state = round( + self._attr_native_value = round( self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2 ) elif self.type == "up_max_bandwidth": - self._attr_state = round( + self._attr_native_value = round( self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2 ) elif self.type == "current_down_bandwidth": - self._attr_state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) + self._attr_native_value = round( + self.bbox_data.data["rx"]["bandwidth"] / 1000, 2 + ) elif self.type == "current_up_bandwidth": - self._attr_state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + self._attr_native_value = round( + self.bbox_data.data["tx"]["bandwidth"] / 1000, 2 + ) elif self.type == "number_of_reboots": - self._attr_state = self.bbox_data.router_infos["device"]["numberofboots"] + self._attr_native_value = self.bbox_data.router_infos["device"][ + "numberofboots" + ] class BboxData: diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 2ed6b71be41..9ec81956c56 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -63,17 +63,17 @@ class BeewiSmartclimSensor(SensorEntity): self._poller = poller self._attr_name = name self._device = device - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._attr_device_class = self._device self._attr_unique_id = f"{mac}_{device}" def update(self): """Fetch new state data from the poller.""" self._poller.update_sensor() - self._attr_state = None + self._attr_native_value = None if self._device == DEVICE_CLASS_TEMPERATURE: - self._attr_state = self._poller.get_temperature() + self._attr_native_value = self._poller.get_temperature() if self._device == DEVICE_CLASS_HUMIDITY: - self._attr_state = self._poller.get_humidity() + self._attr_native_value = self._poller.get_humidity() if self._device == DEVICE_CLASS_BATTERY: - self._attr_state = self._poller.get_battery() + self._attr_native_value = self._poller.get_battery() diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 8a1f8c60ccf..ad5ca13684a 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -101,7 +101,7 @@ class BH1750Sensor(SensorEntity): def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): """Initialize the sensor.""" self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._multiplier = multiplier self.bh1750_sensor = bh1750_sensor @@ -109,7 +109,7 @@ class BH1750Sensor(SensorEntity): """Get the latest data from the BH1750 and update the states.""" await self.hass.async_add_executor_job(self.bh1750_sensor.update) if self.bh1750_sensor.sample_ok and self.bh1750_sensor.light_level >= 0: - self._attr_state = int( + self._attr_native_value = int( round(self.bh1750_sensor.light_level * self._multiplier) ) else: diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 2bd5de34d51..87d574fc4b0 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -92,6 +92,9 @@ DEVICE_CLASS_SMOKE = "smoke" # On means sound detected, Off means no sound (clear) DEVICE_CLASS_SOUND = "sound" +# On means update available, Off means up-to-date +DEVICE_CLASS_UPDATE = "update" + # On means vibration detected, Off means no vibration DEVICE_CLASS_VIBRATION = "vibration" @@ -121,6 +124,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index eed5c3f5896..309e26847a1 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -37,6 +37,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, DOMAIN, @@ -82,6 +83,8 @@ CONF_IS_SMOKE = "is_smoke" CONF_IS_NO_SMOKE = "is_no_smoke" CONF_IS_SOUND = "is_sound" CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_UPDATE = "is_update" +CONF_IS_NO_UPDATE = "is_no_update" CONF_IS_VIBRATION = "is_vibration" CONF_IS_NO_VIBRATION = "is_no_vibration" CONF_IS_OPEN = "is_open" @@ -107,6 +110,7 @@ IS_ON = [ CONF_IS_PROBLEM, CONF_IS_SMOKE, CONF_IS_SOUND, + CONF_IS_UPDATE, CONF_IS_UNSAFE, CONF_IS_VIBRATION, CONF_IS_ON, @@ -133,6 +137,7 @@ IS_OFF = [ CONF_IS_NO_PROBLEM, CONF_IS_NO_SMOKE, CONF_IS_NO_SOUND, + CONF_IS_NO_UPDATE, CONF_IS_NO_VIBRATION, CONF_IS_OFF, ] @@ -187,6 +192,7 @@ ENTITY_CONDITIONS = { DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_IS_UPDATE}, {CONF_TYPE: CONF_IS_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_IS_VIBRATION}, {CONF_TYPE: CONF_IS_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index ad5c26ed04f..a0966b5a018 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -35,6 +35,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, DOMAIN, @@ -82,6 +83,8 @@ CONF_SMOKE = "smoke" CONF_NO_SMOKE = "no_smoke" CONF_SOUND = "sound" CONF_NO_SOUND = "no_sound" +CONF_UPDATE = "update" +CONF_NO_UPDATE = "no_update" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" CONF_OPENED = "opened" @@ -108,6 +111,7 @@ TURNED_ON = [ CONF_SMOKE, CONF_SOUND, CONF_UNSAFE, + CONF_UPDATE, CONF_VIBRATION, CONF_TURNED_ON, ] @@ -169,6 +173,7 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_UPDATE}, {CONF_TYPE: CONF_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 7380d1be576..62b6ec20323 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -38,6 +38,8 @@ "is_no_smoke": "{entity_name} is not detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_no_sound": "{entity_name} is not detecting sound", + "is_update": "{entity_name} has an update available", + "is_no_update": "{entity_name} is up-to-date", "is_vibration": "{entity_name} is detecting vibration", "is_no_vibration": "{entity_name} is not detecting vibration", "is_open": "{entity_name} is open", @@ -82,6 +84,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", + "update": "{entity_name} got an update available", + "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", @@ -175,6 +179,10 @@ "off": "[%key:component::binary_sensor::state::gas::off%]", "on": "[%key:component::binary_sensor::state::gas::on%]" }, + "update": { + "off": "Up-to-date", + "on": "Update available" + }, "vibration": { "off": "[%key:component::binary_sensor::state::gas::off%]", "on": "[%key:component::binary_sensor::state::gas::on%]" diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 9c92a50246a..089f72f51d5 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} no est\u00e0 detectant cap problema", "is_no_smoke": "{entity_name} no detecta fum", "is_no_sound": "{entity_name} no detecta so", + "is_no_update": "{entity_name} est\u00e0 actualitzat/da", "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", "is_not_bat_low": "Bateria de {entity_name} normal", "is_not_cold": "{entity_name} no est\u00e0 fred", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", "is_unsafe": "{entity_name} \u00e9s insegur", + "is_update": "{entity_name} t\u00e9 una actualitzaci\u00f3 disponible", "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} ha deixat de detectar un problema", "no_smoke": "{entity_name} ha deixat de detectar fum", "no_sound": "{entity_name} ha deixat de detectar so", + "no_update": "{entity_name} s'ha actualitzat", "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", "not_bat_low": "Bateria de {entity_name} normal", "not_cold": "{entity_name} es torna no-fred", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s", "unsafe": "{entity_name} es torna insegur", + "update": "{entity_name} obt\u00e9 una nova actualitzaci\u00f3 disponible", "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" } }, @@ -178,6 +182,10 @@ "off": "Lliure", "on": "Detectat" }, + "update": { + "off": "Actualitzat/da", + "on": "Actualitzaci\u00f3 disponible" + }, "vibration": { "off": "Lliure", "on": "Detectat" diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index 90f25332bdb..25b82e54de7 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} nehl\u00e1s\u00ed probl\u00e9m", "is_no_smoke": "{entity_name} nedetekuje kou\u0159", "is_no_sound": "{entity_name} nedetekuje zvuk", + "is_no_update": "{entity_name} je aktu\u00e1ln\u00ed", "is_no_vibration": "{entity_name} nedetekuje vibrace", "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", "is_not_cold": "{entity_name} nen\u00ed studen\u00fd", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} detekuje kou\u0159", "is_sound": "{entity_name} detekuje zvuk", "is_unsafe": "{entity_name} nen\u00ed bezpe\u010dno", + "is_update": "{entity_name} m\u00e1 k dispozici aktualizaci", "is_vibration": "{entity_name} detekuje vibrace" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} p\u0159estalo detekovat probl\u00e9m", "no_smoke": "{entity_name} p\u0159estalo detekovat kou\u0159", "no_sound": "{entity_name} p\u0159estalo detekovat zvuk", + "no_update": "{entity_name} se stalo aktu\u00e1ln\u00ed", "no_vibration": "{entity_name} p\u0159estalo detekovat vibrace", "not_bat_low": "{entity_name} baterie v norm\u00e1lu", "not_cold": "{entity_name} p\u0159estal b\u00fdt studen\u00fd", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} vypnuto", "turned_on": "{entity_name} zapnuto", "unsafe": "{entity_name} hl\u00e1s\u00ed ohro\u017een\u00ed", + "update": "{entity_name} m\u00e1 k dispozici aktualizaci", "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, @@ -178,6 +182,10 @@ "off": "Ticho", "on": "Zachycen zvuk" }, + "update": { + "off": "Aktu\u00e1ln\u00ed", + "on": "Aktualizace k dispozici" + }, "vibration": { "off": "Klid", "on": "Zji\u0161t\u011bny vibrace" diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index a2ef817bedb..21d1eff1ebf 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} erkennt kein Problem", "is_no_smoke": "{entity_name} erkennt keinen Rauch", "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche", + "is_no_update": "{entity_name} ist aktuell", "is_no_vibration": "{entity_name} erkennt keine Vibrationen", "is_not_bat_low": "{entity_name} Batterie ist normal", "is_not_cold": "{entity_name} ist nicht kalt", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} hat Rauch detektiert", "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", "is_unsafe": "{entity_name} ist unsicher", + "is_update": "{entity_name} hat ein Update verf\u00fcgbar", "is_vibration": "{entity_name} erkennt Vibrationen." }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} hat kein Problem mehr erkannt", "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt", "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt", + "no_update": "{entity_name} wurde auf den neuesten Stand gebracht", "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", "not_bat_low": "{entity_name} Batterie normal", "not_cold": "{entity_name} w\u00e4rmte auf", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet", "unsafe": "{entity_name} ist unsicher", + "update": "{entity_name} hat ein Update verf\u00fcgbar", "vibration": "{entity_name} detektiert Vibrationen" } }, @@ -178,6 +182,10 @@ "off": "Normal", "on": "Erkannt" }, + "update": { + "off": "Aktuell", + "on": "Update verf\u00fcgbar" + }, "vibration": { "off": "Normal", "on": "Erkannt" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 98c8a3a220a..047820498da 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} is not detecting problem", "is_no_smoke": "{entity_name} is not detecting smoke", "is_no_sound": "{entity_name} is not detecting sound", + "is_no_update": "{entity_name} is up-to-date", "is_no_vibration": "{entity_name} is not detecting vibration", "is_not_bat_low": "{entity_name} battery is normal", "is_not_cold": "{entity_name} is not cold", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} is detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_unsafe": "{entity_name} is unsafe", + "is_update": "{entity_name} has an update available", "is_vibration": "{entity_name} is detecting vibration" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} stopped detecting problem", "no_smoke": "{entity_name} stopped detecting smoke", "no_sound": "{entity_name} stopped detecting sound", + "no_update": "{entity_name} became up-to-date", "no_vibration": "{entity_name} stopped detecting vibration", "not_bat_low": "{entity_name} battery normal", "not_cold": "{entity_name} became not cold", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on", "unsafe": "{entity_name} became unsafe", + "update": "{entity_name} got an update available", "vibration": "{entity_name} started detecting vibration" } }, @@ -178,6 +182,10 @@ "off": "Clear", "on": "Detected" }, + "update": { + "off": "Up-to-date", + "on": "Update available" + }, "vibration": { "off": "Clear", "on": "Detected" diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 99fbec0b89e..2a0172300c9 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} ei leia probleemi", "is_no_smoke": "{entity_name} ei tuvasta suitsu", "is_no_sound": "{entity_name} ei tuvasta heli", + "is_no_update": "{entity_name} on ajakohane", "is_no_vibration": "{entity_name} ei tuvasta vibratsiooni", "is_not_bat_low": "{entity_name} aku on laetud", "is_not_cold": "{entity_name} ei ole k\u00fclm", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} tuvastab suitsu", "is_sound": "{entity_name} tuvastab heli", "is_unsafe": "{entity_name} on ebaturvaline", + "is_update": "{entity_name} on saadaval uuendus", "is_vibration": "{entity_name} tuvastab vibratsiooni" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} l\u00f5petas probleemi tuvastamise", "no_smoke": "{entity_name} l\u00f5petas suitsu tuvastamise", "no_sound": "{entity_name} l\u00f5petas heli tuvastamise", + "no_update": "{entity_name} on uuendatud", "no_vibration": "{entity_name} l\u00f5petas vibratsiooni tuvastamise", "not_bat_low": "{entity_name} aku on laetud", "not_cold": "{entity_name} ei ole enam k\u00fclm", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse", "unsafe": "{entity_name} on ebaturvaline", + "update": "{entity_name} sai saadavaloleva uuenduse", "vibration": "{entity_name} registreeris vibratsiooni" } }, @@ -178,6 +182,10 @@ "off": "Puudub", "on": "Tuvastatud" }, + "update": { + "off": "Ajakohane", + "on": "Saadaval on uuendus" + }, "vibration": { "off": "Puudub", "on": "Tuvastatud" diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index ede13a68dc9..aa0686c0375 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_update": "{entity_name} est \u00e0 jour", "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", "is_not_bat_low": "{entity_name} batterie normale", "is_not_cold": "{entity_name} n'est pas froid", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", "is_sound": "{entity_name} d\u00e9tecte du son", "is_unsafe": "{entity_name} est dangereux", + "is_update": "{entity_name} a une mise \u00e0 jour disponible", "is_vibration": "{entity_name} d\u00e9tecte des vibrations" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", + "no_update": "{entity_name} a \u00e9t\u00e9 mis \u00e0 jour", "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", "not_bat_low": "{entity_name} batterie normale", "not_cold": "{entity_name} n'est plus froid", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", + "update": "{entity_name} a une mise \u00e0 jour disponible", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } }, @@ -178,6 +182,10 @@ "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, + "update": { + "off": "\u00c0 jour", + "on": "Mise \u00e0 jour disponible" + }, "vibration": { "off": "RAS", "on": "D\u00e9tect\u00e9e" diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index c4395ca806c..d8befd7ae35 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} nem \u00e9szlel probl\u00e9m\u00e1t", "is_no_smoke": "{entity_name} nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", "is_no_sound": "{entity_name} nem \u00e9rz\u00e9kel hangot", + "is_no_update": "{entity_name} naprak\u00e9sz", "is_no_vibration": "{entity_name} nem \u00e9rz\u00e9kel rezg\u00e9st", "is_not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "is_not_cold": "{entity_name} nem hideg", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_update": "{entity_name} egy friss\u00edt\u00e9s \u00e1ll rendelkez\u00e9sre", "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} m\u00e1r nem \u00e9szlel probl\u00e9m\u00e1t", "no_smoke": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", "no_sound": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel hangot", + "no_update": "{entity_name} naprak\u00e9sz lett", "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "not_cold": "{entity_name} m\u00e1r nem hideg", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva", "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "update": "{entity_name} el\u00e9rhet\u0151 friss\u00edt\u00e9s", "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" } }, @@ -178,6 +182,10 @@ "off": "Norm\u00e1l", "on": "\u00c9szlelve" }, + "update": { + "off": "Naprak\u00e9sz", + "on": "Friss\u00edt\u00e9s el\u00e9rhet\u0151" + }, "vibration": { "off": "Norm\u00e1l", "on": "\u00c9szlelve" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 68c427cbc04..b6301ed8f62 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} non sta rilevando un problema", "is_no_smoke": "{entity_name} non sta rilevando il fumo", "is_no_sound": "{entity_name} non sta rilevando il suono", + "is_no_update": "{entity_name} \u00e8 aggiornato", "is_no_vibration": "{entity_name} non sta rilevando la vibrazione", "is_not_bat_low": "{entity_name} la batteria \u00e8 normale", "is_not_cold": "{entity_name} non \u00e8 freddo", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} sta rilevando il fumo", "is_sound": "{entity_name} sta rilevando il suono", "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_update": "{entity_name} ha un aggiornamento disponibile", "is_vibration": "{entity_name} sta rilevando la vibrazione" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} ha smesso di rilevare un problema", "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", "no_sound": "{entity_name} ha smesso di rilevare il suono", + "no_update": "{entity_name} \u00e8 diventato aggiornato", "no_vibration": "{entity_name} ha smesso di rilevare le vibrazioni", "not_bat_low": "{entity_name} batteria normale", "not_cold": "{entity_name} non \u00e8 diventato freddo", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato", "unsafe": "{entity_name} diventato non sicuro", + "update": "{entity_name} ha ottenuto un aggiornamento disponibile", "vibration": "{entity_name} iniziato a rilevare le vibrazioni" } }, @@ -178,6 +182,10 @@ "off": "Assente", "on": "Rilevato" }, + "update": { + "off": "Aggiornato", + "on": "Aggiornamento disponibile" + }, "vibration": { "off": "Assente", "on": "Rilevata" diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 9352bfa8d47..b44dd3449eb 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -42,6 +42,7 @@ "is_smoke": "{entity_name} detecteert rook", "is_sound": "{entity_name} detecteert geluid", "is_unsafe": "{entity_name} is onveilig", + "is_update": "{entity_name} heeft een update beschikbaar", "is_vibration": "{entity_name} detecteert trillingen" }, "trigger_type": { @@ -86,6 +87,7 @@ "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld", "unsafe": "{entity_name} werd onveilig", + "update": "{entity_name} kreeg een update beschikbaar", "vibration": "{entity_name} begon trillingen te detecteren" } }, @@ -178,6 +180,9 @@ "off": "Niet gedetecteerd", "on": "Gedetecteerd" }, + "update": { + "on": "Update beschikbaar" + }, "vibration": { "off": "Niet gedetecteerd", "on": "Gedetecteerd" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 023fec6cc39..041643f9cc3 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} registrerer ikke et problem", "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk", "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_update": "{entity_name} er oppdatert", "is_no_vibration": "{entity_name} registrerer ikke bevegelse", "is_not_bat_low": "{entity_name} batteri er normalt", "is_not_cold": "{entity_name} er ikke kald", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} registrerer r\u00f8yk", "is_sound": "{entity_name} registrerer lyd", "is_unsafe": "{entity_name} er utrygg", + "is_update": "{entity_name} har en tilgjengelig oppdatering", "is_vibration": "{entity_name} registrerer vibrasjon" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} sluttet \u00e5 registrere problem", "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk", "no_sound": "{entity_name} sluttet \u00e5 registrere lyd", + "no_update": "{entity_name} ble oppdatert", "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", "not_bat_low": "{entity_name} batteri normalt", "not_cold": "{entity_name} ble ikke lenger kald", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5", "unsafe": "{entity_name} ble usikker", + "update": "{entity_name} har en oppdatering tilgjengelig", "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" } }, @@ -178,6 +182,10 @@ "off": "Klart", "on": "Oppdaget" }, + "update": { + "off": "Oppdatert", + "on": "Oppdatering tilgjengelig" + }, "vibration": { "off": "Klart", "on": "Oppdaget" diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 2db1506b392..c245d2ba15a 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_no_update": "{entity_name} \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_update": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "no_update": "{entity_name} \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f", "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434", "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "update": "\u0421\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" } }, @@ -178,6 +182,10 @@ "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" }, + "update": { + "off": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f", + "on": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" + }, "vibration": { "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index bf50782743e..4733d4d1dcc 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name}\u672a\u5075\u6e2c\u5230\u554f\u984c", "is_no_smoke": "{entity_name}\u672a\u5075\u6e2c\u5230\u7159\u9727", "is_no_sound": "{entity_name}\u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_update": "{entity_name} \u5df2\u6700\u65b0", "is_no_vibration": "{entity_name}\u672a\u5075\u6e2c\u5230\u9707\u52d5", "is_not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", "is_not_cold": "{entity_name}\u4e0d\u51b7", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name}\u6b63\u5075\u6e2c\u5230\u7159\u9727", "is_sound": "{entity_name}\u6b63\u5075\u6e2c\u5230\u8072\u97f3", "is_unsafe": "{entity_name}\u4e0d\u5b89\u5168", + "is_update": "{entity_name} \u6709\u66f4\u65b0", "is_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", "no_smoke": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", "no_sound": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_update": "{entity_name} \u5df2\u6700\u65b0", "no_vibration": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", "not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", "not_cold": "{entity_name}\u5df2\u4e0d\u51b7", @@ -86,6 +89,7 @@ "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f", "unsafe": "{entity_name}\u5df2\u4e0d\u5b89\u5168", + "update": "{entity_name} \u6709\u66f4\u65b0", "vibration": "{entity_name}\u5df2\u5075\u6e2c\u5230\u9707\u52d5" } }, @@ -178,6 +182,10 @@ "off": "\u672a\u89f8\u767c", "on": "\u5df2\u89f8\u767c" }, + "update": { + "off": "\u5df2\u6700\u65b0", + "on": "\u6709\u66f4\u65b0" + }, "vibration": { "off": "\u672a\u5075\u6e2c", "on": "\u5075\u6e2c" diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index d11c2a2b726..b66f775eae2 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,11 +1,17 @@ """Bitcoin information service that uses blockchain.com.""" +from __future__ import annotations + from datetime import timedelta import logging from blockchain import exchangerates, statistics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CURRENCY, @@ -25,34 +31,112 @@ ICON = "mdi:currency-btc" SCAN_INTERVAL = timedelta(minutes=5) -OPTION_TYPES = { - "exchangerate": ["Exchange rate (1 BTC)", None], - "trade_volume_btc": ["Trade volume", "BTC"], - "miners_revenue_usd": ["Miners revenue", "USD"], - "btc_mined": ["Mined", "BTC"], - "trade_volume_usd": ["Trade volume", "USD"], - "difficulty": ["Difficulty", None], - "minutes_between_blocks": ["Time between Blocks", TIME_MINUTES], - "number_of_transactions": ["No. of Transactions", None], - "hash_rate": ["Hash rate", f"PH/{TIME_SECONDS}"], - "timestamp": ["Timestamp", None], - "mined_blocks": ["Mined Blocks", None], - "blocks_size": ["Block size", None], - "total_fees_btc": ["Total fees", "BTC"], - "total_btc_sent": ["Total sent", "BTC"], - "estimated_btc_sent": ["Estimated sent", "BTC"], - "total_btc": ["Total", "BTC"], - "total_blocks": ["Total Blocks", None], - "next_retarget": ["Next retarget", None], - "estimated_transaction_volume_usd": ["Est. Transaction volume", "USD"], - "miners_revenue_btc": ["Miners revenue", "BTC"], - "market_price_usd": ["Market price", "USD"], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="exchangerate", + name="Exchange rate (1 BTC)", + ), + SensorEntityDescription( + key="trade_volume_btc", + name="Trade volume", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="miners_revenue_usd", + name="Miners revenue", + native_unit_of_measurement="USD", + ), + SensorEntityDescription( + key="btc_mined", + name="Mined", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="trade_volume_usd", + name="Trade volume", + native_unit_of_measurement="USD", + ), + SensorEntityDescription( + key="difficulty", + name="Difficulty", + ), + SensorEntityDescription( + key="minutes_between_blocks", + name="Time between Blocks", + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + key="number_of_transactions", + name="No. of Transactions", + ), + SensorEntityDescription( + key="hash_rate", + name="Hash rate", + native_unit_of_measurement=f"PH/{TIME_SECONDS}", + ), + SensorEntityDescription( + key="timestamp", + name="Timestamp", + ), + SensorEntityDescription( + key="mined_blocks", + name="Mined Blocks", + ), + SensorEntityDescription( + key="blocks_size", + name="Block size", + ), + SensorEntityDescription( + key="total_fees_btc", + name="Total fees", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_btc_sent", + name="Total sent", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="estimated_btc_sent", + name="Estimated sent", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_btc", + name="Total", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_blocks", + name="Total Blocks", + ), + SensorEntityDescription( + key="next_retarget", + name="Next retarget", + ), + SensorEntityDescription( + key="estimated_transaction_volume_usd", + name="Est. Transaction volume", + native_unit_of_measurement="USD", + ), + SensorEntityDescription( + key="miners_revenue_btc", + name="Miners revenue", + native_unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="market_price_usd", + name="Market price", + native_unit_of_measurement="USD", + ), +) + +OPTION_KEYS = [desc.key for desc in SENSOR_TYPES] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(OPTION_TYPES)] + cv.ensure_list, [vol.In(OPTION_KEYS)] ), vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, } @@ -69,11 +153,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): currency = DEFAULT_CURRENCY data = BitcoinData() - dev = [] - for variable in config[CONF_DISPLAY_OPTIONS]: - dev.append(BitcoinSensor(data, variable, currency)) + entities = [ + BitcoinSensor(data, currency, description) + for description in SENSOR_TYPES + if description.key in config[CONF_DISPLAY_OPTIONS] + ] - add_entities(dev, True) + add_entities(entities, True) class BitcoinSensor(SensorEntity): @@ -82,13 +168,11 @@ class BitcoinSensor(SensorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON - def __init__(self, data, option_type, currency): + def __init__(self, data, currency, description: SensorEntityDescription): """Initialize the sensor.""" + self.entity_description = description self.data = data - self._attr_name = OPTION_TYPES[option_type][0] - self._attr_unit_of_measurement = OPTION_TYPES[option_type][1] self._currency = currency - self.type = option_type def update(self): """Get the latest data and updates the states.""" @@ -96,49 +180,50 @@ class BitcoinSensor(SensorEntity): stats = self.data.stats ticker = self.data.ticker - if self.type == "exchangerate": - self._attr_state = ticker[self._currency].p15min - self._attr_unit_of_measurement = self._currency - elif self.type == "trade_volume_btc": - self._attr_state = f"{stats.trade_volume_btc:.1f}" - elif self.type == "miners_revenue_usd": - self._attr_state = f"{stats.miners_revenue_usd:.0f}" - elif self.type == "btc_mined": - self._attr_state = str(stats.btc_mined * 0.00000001) - elif self.type == "trade_volume_usd": - self._attr_state = f"{stats.trade_volume_usd:.1f}" - elif self.type == "difficulty": - self._attr_state = f"{stats.difficulty:.0f}" - elif self.type == "minutes_between_blocks": - self._attr_state = f"{stats.minutes_between_blocks:.2f}" - elif self.type == "number_of_transactions": - self._attr_state = str(stats.number_of_transactions) - elif self.type == "hash_rate": - self._attr_state = f"{stats.hash_rate * 0.000001:.1f}" - elif self.type == "timestamp": - self._attr_state = stats.timestamp - elif self.type == "mined_blocks": - self._attr_state = str(stats.mined_blocks) - elif self.type == "blocks_size": - self._attr_state = f"{stats.blocks_size:.1f}" - elif self.type == "total_fees_btc": - self._attr_state = f"{stats.total_fees_btc * 0.00000001:.2f}" - elif self.type == "total_btc_sent": - self._attr_state = f"{stats.total_btc_sent * 0.00000001:.2f}" - elif self.type == "estimated_btc_sent": - self._attr_state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" - elif self.type == "total_btc": - self._attr_state = f"{stats.total_btc * 0.00000001:.2f}" - elif self.type == "total_blocks": - self._attr_state = f"{stats.total_blocks:.0f}" - elif self.type == "next_retarget": - self._attr_state = f"{stats.next_retarget:.2f}" - elif self.type == "estimated_transaction_volume_usd": - self._attr_state = f"{stats.estimated_transaction_volume_usd:.2f}" - elif self.type == "miners_revenue_btc": - self._attr_state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" - elif self.type == "market_price_usd": - self._attr_state = f"{stats.market_price_usd:.2f}" + sensor_type = self.entity_description.key + if sensor_type == "exchangerate": + self._attr_native_value = ticker[self._currency].p15min + self._attr_native_unit_of_measurement = self._currency + elif sensor_type == "trade_volume_btc": + self._attr_native_value = f"{stats.trade_volume_btc:.1f}" + elif sensor_type == "miners_revenue_usd": + self._attr_native_value = f"{stats.miners_revenue_usd:.0f}" + elif sensor_type == "btc_mined": + self._attr_native_value = str(stats.btc_mined * 0.00000001) + elif sensor_type == "trade_volume_usd": + self._attr_native_value = f"{stats.trade_volume_usd:.1f}" + elif sensor_type == "difficulty": + self._attr_native_value = f"{stats.difficulty:.0f}" + elif sensor_type == "minutes_between_blocks": + self._attr_native_value = f"{stats.minutes_between_blocks:.2f}" + elif sensor_type == "number_of_transactions": + self._attr_native_value = str(stats.number_of_transactions) + elif sensor_type == "hash_rate": + self._attr_native_value = f"{stats.hash_rate * 0.000001:.1f}" + elif sensor_type == "timestamp": + self._attr_native_value = stats.timestamp + elif sensor_type == "mined_blocks": + self._attr_native_value = str(stats.mined_blocks) + elif sensor_type == "blocks_size": + self._attr_native_value = f"{stats.blocks_size:.1f}" + elif sensor_type == "total_fees_btc": + self._attr_native_value = f"{stats.total_fees_btc * 0.00000001:.2f}" + elif sensor_type == "total_btc_sent": + self._attr_native_value = f"{stats.total_btc_sent * 0.00000001:.2f}" + elif sensor_type == "estimated_btc_sent": + self._attr_native_value = f"{stats.estimated_btc_sent * 0.00000001:.2f}" + elif sensor_type == "total_btc": + self._attr_native_value = f"{stats.total_btc * 0.00000001:.2f}" + elif sensor_type == "total_blocks": + self._attr_native_value = f"{stats.total_blocks:.0f}" + elif sensor_type == "next_retarget": + self._attr_native_value = f"{stats.next_retarget:.2f}" + elif sensor_type == "estimated_transaction_volume_usd": + self._attr_native_value = f"{stats.estimated_transaction_volume_usd:.2f}" + elif sensor_type == "miners_revenue_btc": + self._attr_native_value = f"{stats.miners_revenue_btc * 0.00000001:.1f}" + elif sensor_type == "market_price_usd": + self._attr_native_value = f"{stats.market_price_usd:.2f}" class BitcoinData: diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index 16f247693af..f83751fb503 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BizkaibusSensor(SensorEntity): """The class for handling the data.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, data, name): """Initialize the sensor.""" @@ -48,7 +48,7 @@ class BizkaibusSensor(SensorEntity): """Get the latest data from the webservice.""" self.data.update() with suppress(TypeError): - self._attr_state = self.data.info[0][ATTR_DUE_IN] + self._attr_native_value = self.data.info[0][ATTR_DUE_IN] class Bizkaibus: diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 09bfca88776..200661dcd1c 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -20,10 +20,10 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): def __init__(self, feature): """Initialize a BleBox sensor feature.""" super().__init__(feature) - self._attr_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] + self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] @property - def state(self): + def native_value(self): """Return the state.""" return self._feature.current diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 97a6c1bdc18..ce51a8a0967 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a(z) {address} c\u00edmen.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { @@ -14,7 +15,9 @@ "data": { "host": "IP c\u00edm", "port": "Port" - } + }, + "description": "\u00c1ll\u00edtsa be a BleBox k\u00e9sz\u00fcl\u00e9ket a Homeassistantba val\u00f3 integr\u00e1ci\u00f3hoz.", + "title": "\u00c1ll\u00edtsa be a BleBox eszk\u00f6zt" } } } diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index f9b8ec31605..6be284e2197 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,47 +1,60 @@ """Support for Blink system camera control.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_MOTION, BinarySensorEntity, + BinarySensorEntityDescription, ) from .const import DOMAIN, TYPE_BATTERY, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED -BINARY_SENSORS = { - TYPE_BATTERY: ["Battery", DEVICE_CLASS_BATTERY], - TYPE_CAMERA_ARMED: ["Camera Armed", None], - TYPE_MOTION_DETECTED: ["Motion Detected", DEVICE_CLASS_MOTION], -} +BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key=TYPE_BATTERY, + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + ), + BinarySensorEntityDescription( + key=TYPE_CAMERA_ARMED, + name="Camera Armed", + ), + BinarySensorEntityDescription( + key=TYPE_MOTION_DETECTED, + name="Motion Detected", + device_class=DEVICE_CLASS_MOTION, + ), +) async def async_setup_entry(hass, config, async_add_entities): """Set up the blink binary sensors.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for camera in data.cameras: - for sensor_type in BINARY_SENSORS: - entities.append(BlinkBinarySensor(data, camera, sensor_type)) + entities = [ + BlinkBinarySensor(data, camera, description) + for camera in data.cameras + for description in BINARY_SENSORS_TYPES + ] async_add_entities(entities) class BlinkBinarySensor(BinarySensorEntity): """Representation of a Blink binary sensor.""" - def __init__(self, data, camera, sensor_type): + def __init__(self, data, camera, description: BinarySensorEntityDescription): """Initialize the sensor.""" self.data = data - self._type = sensor_type - name, device_class = BINARY_SENSORS[sensor_type] - self._attr_name = f"{DOMAIN} {camera} {name}" - self._attr_device_class = device_class + self.entity_description = description + self._attr_name = f"{DOMAIN} {camera} {description.name}" self._camera = data.cameras[camera] - self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" + self._attr_unique_id = f"{self._camera.serial}-{description.key}" def update(self): """Update sensor state.""" self.data.refresh() - state = self._camera.attributes[self._type] - if self._type == TYPE_BATTERY: + state = self._camera.attributes[self.entity_description.key] + if self.entity_description.key == TYPE_BATTERY: state = state != "ok" self._attr_is_on = state diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index e2216dc8785..8b4f1ba4eec 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,4 +1,6 @@ """Support for Blink system camera.""" +from __future__ import annotations + import logging from homeassistant.components.camera import Camera @@ -65,6 +67,8 @@ class BlinkCamera(Camera): self._camera.snap_picture() self.data.refresh() - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return self._camera.image_from_cache.content diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 1f7cad3f872..d2122b59cd8 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,7 +1,9 @@ """Support for Blink system camera sensors.""" +from __future__ import annotations + import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, @@ -13,23 +15,30 @@ from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH _LOGGER = logging.getLogger(__name__) -SENSORS = { - TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE], - TYPE_WIFI_STRENGTH: [ - "Wifi Signal", - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - DEVICE_CLASS_SIGNAL_STRENGTH, - ], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=TYPE_TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_FAHRENHEIT, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=TYPE_WIFI_STRENGTH, + name="Wifi Signal", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), +) async def async_setup_entry(hass, config, async_add_entities): """Initialize a Blink sensor.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for camera in data.cameras: - for sensor_type in SENSORS: - entities.append(BlinkSensor(data, camera, sensor_type)) + entities = [ + BlinkSensor(data, camera, description) + for camera in data.cameras + for description in SENSOR_TYPES + ] async_add_entities(entities) @@ -37,26 +46,26 @@ async def async_setup_entry(hass, config, async_add_entities): class BlinkSensor(SensorEntity): """A Blink camera sensor.""" - def __init__(self, data, camera, sensor_type): + def __init__(self, data, camera, description: SensorEntityDescription): """Initialize sensors from Blink camera.""" - name, units, device_class = SENSORS[sensor_type] - self._attr_name = f"{DOMAIN} {camera} {name}" - self._attr_device_class = device_class + self.entity_description = description + self._attr_name = f"{DOMAIN} {camera} {description.name}" self.data = data self._camera = data.cameras[camera] - self._attr_unit_of_measurement = units - self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" + self._attr_unique_id = f"{self._camera.serial}-{description.key}" self._sensor_key = ( - "temperature_calibrated" if sensor_type == "temperature" else sensor_type + "temperature_calibrated" + if description.key == "temperature" + else description.key ) def update(self): """Retrieve sensor data from the camera.""" self.data.refresh() try: - self._attr_state = self._camera.attributes[self._sensor_key] + self._attr_native_value = self._camera.attributes[self._sensor_key] except KeyError: - self._attr_state = None + self._attr_native_value = None _LOGGER.error( "%s not a valid camera attribute. Did the API change?", self._sensor_key ) diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json index e56b142a5b0..135a2f7ef2e 100644 --- a/homeassistant/components/blink/translations/hu.json +++ b/homeassistant/components/blink/translations/hu.json @@ -21,7 +21,19 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Jelentkezzen be Blink-fi\u00f3kkal" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "Blink integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa", + "title": "Villog\u00e1si lehet\u0151s\u00e9gek" } } } diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index bbb9c892871..9d31d4c0583 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -48,7 +48,7 @@ class BlockchainSensor(SensorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON - _attr_unit_of_measurement = "BTC" + _attr_native_unit_of_measurement = "BTC" def __init__(self, name, addresses): """Initialize the sensor.""" @@ -57,4 +57,4 @@ class BlockchainSensor(SensorEntity): def update(self): """Get the latest state of the sensor.""" - self._attr_state = get_balance(self.addresses) + self._attr_native_value = get_balance(self.addresses) diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 570842b9c66..a7255a74d4c 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -1,4 +1,6 @@ """Support for a camera of a BloomSky weather station.""" +from __future__ import annotations + import logging import requests @@ -37,7 +39,9 @@ class BloomSkyCamera(Camera): self._logger = logging.getLogger(__name__) self._attr_unique_id = self._id - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Update the camera's image if it has changed.""" try: self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 7aa2fe9baba..288a1767c7e 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -86,9 +86,13 @@ class BloomSkySensor(SensorEntity): self._sensor_name = sensor_name self._attr_name = f"{device['DeviceName']} {sensor_name}" self._attr_unique_id = f"{self._device_id}-{sensor_name}" - self._attr_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get(sensor_name, None) + self._attr_native_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get( + sensor_name, None + ) if self._bloomsky.is_metric: - self._attr_unit_of_measurement = SENSOR_UNITS_METRIC.get(sensor_name, None) + self._attr_native_unit_of_measurement = SENSOR_UNITS_METRIC.get( + sensor_name, None + ) @property def device_class(self): @@ -99,6 +103,6 @@ class BloomSkySensor(SensorEntity): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] - self._attr_state = ( + self._attr_native_value = ( f"{state:.2f}" if self._sensor_name in FORMAT_NUMBERS else state ) 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: diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 60ce963bf9e..3b9589d0a6a 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -127,11 +127,11 @@ class BME280Sensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self.temp_unit = temp_unit self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.type == SENSOR_TEMP: temperature = round(self.coordinator.data.temperature, 1) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 527a971b237..9669738b2e5 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -327,25 +327,27 @@ class BME680Sensor(SensorEntity): self.bme680_client = bme680_client self.temp_unit = temp_unit self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] async def async_update(self): """Get the latest data from the BME680 and update the states.""" await self.hass.async_add_executor_job(self.bme680_client.update) if self.type == SENSOR_TEMP: - self._attr_state = round(self.bme680_client.sensor_data.temperature, 1) + self._attr_native_value = round( + self.bme680_client.sensor_data.temperature, 1 + ) if self.temp_unit == TEMP_FAHRENHEIT: - self._attr_state = round(celsius_to_fahrenheit(self.state), 1) + self._attr_native_value = round(celsius_to_fahrenheit(self.state), 1) elif self.type == SENSOR_HUMID: - self._attr_state = round(self.bme680_client.sensor_data.humidity, 1) + self._attr_native_value = round(self.bme680_client.sensor_data.humidity, 1) elif self.type == SENSOR_PRESS: - self._attr_state = round(self.bme680_client.sensor_data.pressure, 1) + self._attr_native_value = round(self.bme680_client.sensor_data.pressure, 1) elif self.type == SENSOR_GAS: - self._attr_state = int( + self._attr_native_value = int( round(self.bme680_client.sensor_data.gas_resistance, 0) ) elif self.type == SENSOR_AQ: aq_score = self.bme680_client.sensor_data.air_quality if aq_score is not None: - self._attr_state = round(aq_score, 1) + self._attr_native_value = round(aq_score, 1) diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 7bf355bb736..21ab71e5ce6 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -78,7 +78,7 @@ class Bmp280Sensor(SensorEntity): """Initialize the sensor.""" self._bmp280 = bmp280 self._attr_name = name - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement class Bmp280TemperatureSensor(Bmp280Sensor): @@ -94,7 +94,7 @@ class Bmp280TemperatureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._attr_state = round(self._bmp280.temperature, 1) + self._attr_native_value = round(self._bmp280.temperature, 1) if not self.available: _LOGGER.warning("Communication restored with temperature sensor") self._attr_available = True @@ -119,7 +119,7 @@ class Bmp280PressureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._attr_state = round(self._bmp280.pressure) + self._attr_native_value = round(self._bmp280.pressure) if not self.available: _LOGGER.warning("Communication restored with pressure sensor") self._attr_available = True diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 3bd2365f88e..85a5c9cd02f 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -79,7 +80,7 @@ _SERVICE_MAP = { UNDO_UPDATE_LISTENER = "undo_update_listener" -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the BMW Connected Drive component from configuration.yaml.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config @@ -335,11 +336,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 diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 17aaa166942..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.16"], + "requirements": ["bimmer_connected==0.7.19"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index df899496339..76d183bf8e8 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -516,7 +516,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._attr_device_class = attribute_info.get( attribute, [None, None, None, None] )[1] - self._attr_unit_of_measurement = attribute_info.get( + self._attr_native_unit_of_measurement = attribute_info.get( attribute, [None, None, None, None] )[2] @@ -525,24 +525,24 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state if self._attribute == "charging_status": - self._attr_state = getattr(vehicle_state, self._attribute).value + self._attr_native_value = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) - self._attr_state = round(value_converted) + self._attr_native_value = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) - self._attr_state = round(value_converted) + self._attr_native_value = round(value_converted) elif self._service is None: - self._attr_state = getattr(vehicle_state, self._attribute) + self._attr_native_value = getattr(vehicle_state, self._attribute) elif self._service == SERVICE_LAST_TRIP: vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "date_utc": date_str = getattr(vehicle_last_trip, "date") - self._attr_state = dt_util.parse_datetime(date_str).isoformat() + self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_state = getattr(vehicle_last_trip, self._attribute) + self._attr_native_value = getattr(vehicle_last_trip, self._attribute) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips for attribute in ( @@ -555,13 +555,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): if self._attribute.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) sub_attr = self._attribute.replace(f"{attribute}_", "") - self._attr_state = getattr(attr, sub_attr) + self._attr_native_value = getattr(attr, sub_attr) return if self._attribute == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") - self._attr_state = dt_util.parse_datetime(date_str).isoformat() + self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_state = getattr(vehicle_all_trips, self._attribute) + self._attr_native_value = getattr(vehicle_all_trips, self._attribute) vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 55aa1eb5772..6ea4f3c7065 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -147,7 +147,7 @@ class TemperatureSensor(SHCEntity, SensorEntity): """Representation of an SHC temperature reporting sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC temperature reporting sensor.""" @@ -156,7 +156,7 @@ class TemperatureSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_temperature" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.temperature @@ -165,7 +165,7 @@ class HumiditySensor(SHCEntity, SensorEntity): """Representation of an SHC humidity reporting sensor.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC humidity reporting sensor.""" @@ -174,7 +174,7 @@ class HumiditySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_humidity" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.humidity @@ -183,7 +183,7 @@ class PuritySensor(SHCEntity, SensorEntity): """Representation of an SHC purity reporting sensor.""" _attr_icon = "mdi:molecule-co2" - _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC purity reporting sensor.""" @@ -192,7 +192,7 @@ class PuritySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_purity" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.purity @@ -207,7 +207,7 @@ class AirQualitySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_airquality" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.combined_rating.name @@ -229,7 +229,7 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_temperature_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.temperature_rating.name @@ -244,7 +244,7 @@ class HumidityRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_humidity_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.humidity_rating.name @@ -259,7 +259,7 @@ class PurityRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_purity_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.purity_rating.name @@ -268,7 +268,7 @@ class PowerSensor(SHCEntity, SensorEntity): """Representation of an SHC power reporting sensor.""" _attr_device_class = DEVICE_CLASS_POWER - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC power reporting sensor.""" @@ -277,7 +277,7 @@ class PowerSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_power" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.powerconsumption @@ -286,7 +286,7 @@ class EnergySensor(SHCEntity, SensorEntity): """Representation of an SHC energy reporting sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC energy reporting sensor.""" @@ -295,7 +295,7 @@ class EnergySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{self._device.serial}_energy" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.energyconsumption / 1000.0 @@ -304,7 +304,7 @@ class ValveTappetSensor(SHCEntity, SensorEntity): """Representation of an SHC valve tappet reporting sensor.""" _attr_icon = "mdi:gauge" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC valve tappet reporting sensor.""" @@ -313,7 +313,7 @@ class ValveTappetSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_valvetappet" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.position diff --git a/homeassistant/components/bosch_shc/translations/zh-Hans.json b/homeassistant/components/bosch_shc/translations/zh-Hans.json new file mode 100644 index 00000000000..46682f56114 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "pairing_failed": "\u914d\u5bf9\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u535a\u4e16 Smart Home Controller \u662f\u5426\u6b63\u5728\u5904\u4e8e\u914d\u5bf9\u6a21\u5f0f(LED \u706f\u95ea\u70c1)\uff0c\u4ee5\u53ca\u952e\u5165\u7684\u5bc6\u7801\u662f\u5426\u6b63\u786e" + }, + "step": { + "credentials": { + "data": { + "password": "Smart Home Controller \u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index fbb23fdee04..5f96af8bad7 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -14,12 +14,14 @@ "data": { "pin": "PIN-k\u00f3d" }, + "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\n Ha a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, l\u00e9pjen a k\u00f6vetkez\u0151re: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "user": { "data": { "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Sony Bravia TV integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/braviatv \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV be van kapcsolva.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index c839a271614..d02d562d55d 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -4,10 +4,20 @@ "authorize": { "data": { "pin": "PIN \u7801" - } + }, + "description": "\u8f93\u5165\u5728 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c\u672a\u663e\u793a PIN \u7801\uff0c\u60a8\u9700\u8981\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002", + "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, "user": { - "description": "\u8bbe\u7f6eSony Bravia\u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002" + "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "title": "Sony Bravia \u7535\u89c6\u9009\u9879" } } } diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 30bc8047d03..f708790a5ce 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -91,10 +91,10 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): self._attr_device_class = SENSOR_TYPES[monitored_condition][2] self._attr_name = f"{device.name} {SENSOR_TYPES[monitored_condition][0]}" self._attr_state_class = SENSOR_TYPES[monitored_condition][3] - self._attr_state = self._coordinator.data[monitored_condition] + self._attr_native_value = self._coordinator.data[monitored_condition] self._attr_unique_id = f"{device.unique_id}-{monitored_condition}" - self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] def _update_state(self, data): """Update the state of the entity.""" - self._attr_state = data[self._monitored_condition] + self._attr_native_value = data[self._monitored_condition] diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 95ffcf063f2..8e34f9f983b 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -87,154 +87,154 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", name=ATTR_PAGE_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", name=ATTR_BW_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", name=ATTR_COLOR_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 0ff5c14d9cc..8dd150b48bf 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -64,7 +64,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" if self.entity_description.key == ATTR_UPTIME: return cast( diff --git a/homeassistant/components/brother/translations/zh-Hans.json b/homeassistant/components/brother/translations/zh-Hans.json index 8f9e85e54a9..91e0c310dd1 100644 --- a/homeassistant/components/brother/translations/zh-Hans.json +++ b/homeassistant/components/brother/translations/zh-Hans.json @@ -1,8 +1,23 @@ { "config": { + "abort": { + "unsupported_model": "\u4e0d\u652f\u6301\u6b64\u6253\u5370\u673a\u578b\u53f7\u3002" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "snmp_error": "SNMP\u670d\u52a1\u5668\u5df2\u5173\u95ed\u6216\u4e0d\u652f\u6301\u6253\u5370\u3002" + }, + "step": { + "user": { + "description": "\u8bbe\u7f6e Brother \u6253\u5370\u673a\u96c6\u6210\u3002\u5982\u679c\u60a8\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/brother" + }, + "zeroconf_confirm": { + "data": { + "type": "\u6253\u5370\u673a\u7c7b\u578b" + }, + "description": "\u60a8\u662f\u5426\u8981\u5c06 Brother \u6253\u5370\u673a {model} (\u5e8f\u5217\u53f7:`{serial_number}`) \u6dfb\u52a0\u5230 Home Assistant ?", + "title": "\u5df2\u53d1\u73b0\u7684 Brother \u6253\u5370\u673a" + } } } } \ No newline at end of file diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index c327f9122ce..22d9ea8e5d8 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -103,4 +103,4 @@ class BrottsplatskartanSensor(SensorEntity): ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION } self._attr_extra_state_attributes.update(incident_counts) - self._attr_state = len(incidents) + self._attr_native_value = len(incidents) diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 499a7d92331..51feb8b75d7 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -11,10 +11,13 @@ "user": { "data": { "host": "Hoszt", + "passkey": "Jelsz\u00f3 karakterl\u00e1nc", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "\u00c1ll\u00edtsa be a BSB-Lan eszk\u00f6zt az HomeAssistantba val\u00f3 integr\u00e1ci\u00f3hoz.", + "title": "Csatlakoz\u00e1s a BSB-Lan eszk\u00f6zh\u00f6z" } } } diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 34f1f173319..91e4bcffb17 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -143,7 +143,9 @@ class BuienradarCam(Camera): _LOGGER.error("Failed to fetch image, %s", type(err)) return False - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """ Return a still image response from the camera. diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 7af84f48af7..1d349fe6f53 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -364,7 +364,7 @@ class BrSensor(SensorEntity): self._attr_name = f"{client_name} {SENSOR_TYPES[sensor_type][0]}" self._attr_icon = SENSOR_TYPES[sensor_type][2] self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._measured = None self._attr_unique_id = "{:2.6f}{:2.6f}{}".format( coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], sensor_type @@ -438,7 +438,7 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) if new_state != self.state or img != self.entity_picture: - self._attr_state = new_state + self._attr_native_value = new_state self._attr_entity_picture = img return True return False @@ -446,9 +446,11 @@ class BrSensor(SensorEntity): if self.type.startswith(WINDSPEED): # hass wants windspeeds in km/h not m/s, so convert: try: - self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._attr_native_value = data.get(FORECAST)[fcday].get( + self.type[:-3] + ) if self.state is not None: - self._attr_state = round(self.state * 3.6, 1) + self._attr_native_value = round(self.state * 3.6, 1) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -456,7 +458,7 @@ class BrSensor(SensorEntity): # update all other sensors try: - self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._attr_native_value = data.get(FORECAST)[fcday].get(self.type[:-3]) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -480,7 +482,7 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) if new_state != self.state or img != self.entity_picture: - self._attr_state = new_state + self._attr_native_value = new_state self._attr_entity_picture = img return True @@ -490,25 +492,27 @@ class BrSensor(SensorEntity): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - self._attr_state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) + self._attr_native_value = nested.get( + self.type[len(PRECIPITATION_FORECAST) + 1 :] + ) return True if self.type in [WINDSPEED, WINDGUST]: # hass wants windspeeds in km/h not m/s, so convert: - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.state is not None: - self._attr_state = round(data.get(self.type) * 3.6, 1) + self._attr_native_value = round(data.get(self.type) * 3.6, 1) return True if self.type == VISIBILITY: # hass wants visibility in km (not m), so convert: - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.state is not None: - self._attr_state = round(self.state / 1000, 1) + self._attr_native_value = round(self.state / 1000, 1) return True # update all other sensors - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)} if self._timeframe is not None: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d1f354cc78e..14cd64df920 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -8,7 +8,9 @@ from collections.abc import Awaitable, Mapping from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta +from functools import partial import hashlib +import inspect import logging import os from random import SystemRandom @@ -62,6 +64,7 @@ from .const import ( DOMAIN, SERVICE_RECORD, ) +from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences # mypy: allow-untyped-calls @@ -138,23 +141,71 @@ async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> return await _async_stream_endpoint_url(hass, camera, fmt) -@bind_hass -async def async_get_image( - hass: HomeAssistant, entity_id: str, timeout: int = 10 +async def _async_get_image( + camera: Camera, + timeout: int = 10, + width: int | None = None, + height: int | None = None, ) -> Image: - """Fetch an image from a camera entity.""" - camera = _get_camera_from_entity_id(hass, entity_id) + """Fetch a snapshot image from a camera. + If width and height are passed, an attempt to scale + the image will be made on a best effort basis. + Not all cameras can scale images or return jpegs + that we can scale, however the majority of cases + are handled. + """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with async_timeout.timeout(timeout): - image = await camera.async_camera_image() + # Calling inspect will be removed in 2022.1 after all + # custom components have had a chance to change their signature + sig = inspect.signature(camera.async_camera_image) + if "height" in sig.parameters and "width" in sig.parameters: + image_bytes = await camera.async_camera_image( + width=width, height=height + ) + else: + _LOGGER.warning( + "The camera entity %s does not support requesting width and height, please open an issue with the integration author", + camera.entity_id, + ) + image_bytes = await camera.async_camera_image() - if image: - return Image(camera.content_type, image) + if image_bytes: + content_type = camera.content_type + image = Image(content_type, image_bytes) + if ( + width is not None + and height is not None + and ("jpeg" in content_type or "jpg" in content_type) + ): + assert width is not None + assert height is not None + return Image( + content_type, scale_jpeg_camera_image(image, width, height) + ) + + return image raise HomeAssistantError("Unable to get image") +@bind_hass +async def async_get_image( + hass: HomeAssistant, + entity_id: str, + timeout: int = 10, + width: int | None = None, + height: int | None = None, +) -> Image: + """Fetch an image from a camera entity. + + width and height will be passed to the underlying camera. + """ + camera = _get_camera_from_entity_id(hass, entity_id) + return await _async_get_image(camera, timeout, width, height) + + @bind_hass async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" @@ -387,12 +438,27 @@ class Camera(Entity): """Return the source of the stream.""" return None - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" raise NotImplementedError() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" + sig = inspect.signature(self.camera_image) + # Calling inspect will be removed in 2022.1 after all + # custom components have had a chance to change their signature + if "height" in sig.parameters and "width" in sig.parameters: + return await self.hass.async_add_executor_job( + partial(self.camera_image, width=width, height=height) + ) + _LOGGER.warning( + "The camera entity %s does not support requesting width and height, please open an issue with the integration author", + self.entity_id, + ) return await self.hass.async_add_executor_job(self.camera_image) async def handle_async_still_stream( @@ -529,14 +595,19 @@ class CameraImageView(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera image.""" - with suppress(asyncio.CancelledError, asyncio.TimeoutError): - async with async_timeout.timeout(CAMERA_IMAGE_TIMEOUT): - image = await camera.async_camera_image() - - if image: - return web.Response(body=image, content_type=camera.content_type) - - raise web.HTTPInternalServerError() + width = request.query.get("width") + height = request.query.get("height") + try: + image = await _async_get_image( + camera, + CAMERA_IMAGE_TIMEOUT, + int(width) if width else None, + int(height) if height else None, + ) + except (HomeAssistantError, ValueError) as ex: + raise web.HTTPInternalServerError() from ex + else: + return web.Response(body=image.content, content_type=image.content_type) class CameraMjpegStream(CameraView): diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/camera/img_util.py similarity index 72% rename from homeassistant/components/homekit/img_util.py rename to homeassistant/components/camera/img_util.py index 7d7a45081a6..4cfb4fda278 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -1,19 +1,32 @@ -"""Image processing for HomeKit component.""" +"""Image processing for cameras.""" import logging +from typing import TYPE_CHECKING, cast SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] _LOGGER = logging.getLogger(__name__) +JPEG_QUALITY = 75 -def scale_jpeg_camera_image(cam_image, width, height): +if TYPE_CHECKING: + from turbojpeg import TurboJPEG + + from . import Image + + +def scale_jpeg_camera_image(cam_image: "Image", width: int, height: int) -> bytes: """Scale a camera image as close as possible to one of the supported scaling factors.""" turbo_jpeg = TurboJPEGSingleton.instance() if not turbo_jpeg: return cam_image.content - (current_width, current_height, _, _) = turbo_jpeg.decode_header(cam_image.content) + try: + (current_width, current_height, _, _) = turbo_jpeg.decode_header( + cam_image.content + ) + except OSError: + return cam_image.content if current_width <= width or current_height <= height: return cam_image.content @@ -26,10 +39,13 @@ def scale_jpeg_camera_image(cam_image, width, height): scaling_factor = supported_sf break - return turbo_jpeg.scale_with_quality( - cam_image.content, - scaling_factor=scaling_factor, - quality=75, + return cast( + bytes, + turbo_jpeg.scale_with_quality( + cam_image.content, + scaling_factor=scaling_factor, + quality=JPEG_QUALITY, + ), ) @@ -45,13 +61,13 @@ class TurboJPEGSingleton: __instance = None @staticmethod - def instance(): + def instance() -> "TurboJPEG": """Singleton for TurboJPEG.""" if TurboJPEGSingleton.__instance is None: TurboJPEGSingleton() return TurboJPEGSingleton.__instance - def __init__(self): + def __init__(self) -> None: """Try to create TurboJPEG only once.""" # pylint: disable=unused-private-member # https://github.com/PyCQA/pylint/issues/4681 diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index ed8e10c1956..6a27999c7fe 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,6 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], + "requirements": ["PyTurboJPEG==1.5.0"], "after_dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 2699ba1f640..a475a27f942 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -1,7 +1,6 @@ """Support for Canary camera.""" from __future__ import annotations -import asyncio from datetime import timedelta from typing import Final @@ -9,9 +8,9 @@ from aiohttp.web import Request, StreamResponse from canary.api import Device, Location from canary.live_stream_api import LiveStreamSession from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol +from homeassistant.components import ffmpeg from homeassistant.components.camera import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, Camera, @@ -123,22 +122,21 @@ class CanaryCamera(CoordinatorEntity, Camera): """Return the camera motion detection status.""" return not self.location.is_recording - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) live_stream_url = await self.hass.async_add_executor_job( getattr, self._live_stream_session, "live_stream_url" ) - - ffmpeg = ImageFrame(self._ffmpeg.binary) - image: bytes | None = await asyncio.shield( - ffmpeg.get_image( - live_stream_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, - ) + return await ffmpeg.async_get_image( + self.hass, + live_stream_url, + extra_cmd=self._ffmpeg_arguments, + width=width, + height=height, ) - return image async def handle_async_mjpeg_stream( self, request: Request diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 5c92f0089f2..1e7747039b8 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))}, @@ -121,7 +120,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): "model": device.device_type["name"], "manufacturer": MANUFACTURER, } - self._attr_unit_of_measurement = sensor_type[1] + self._attr_native_unit_of_measurement = sensor_type[1] self._attr_device_class = sensor_type[3] self._attr_icon = sensor_type[2] @@ -144,6 +143,11 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None + @property + def native_value(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/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 787465bb6f3..7b6445a2f35 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -85,7 +85,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return self.coordinator.data.isoformat() diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index 2ae516565e3..de459c324df 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -4,14 +4,21 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t" }, + "error": { + "connection_refused": "A kapcsolat megtagadva a gazdag\u00e9phez val\u00f3 csatlakoz\u00e1skor", + "connection_timeout": "T\u00fall\u00e9p\u00e9s, amikor ehhez a gazdag\u00e9phez kapcsol\u00f3dik", + "resolve_failed": "Ez a gazdag\u00e9p nem oldhat\u00f3 fel" + }, "step": { "user": { "data": { "host": "Hoszt", "name": "A tan\u00fas\u00edtv\u00e1ny neve", "port": "Port" - } + }, + "title": "Hat\u00e1rozza meg a vizsg\u00e1land\u00f3 tan\u00fas\u00edtv\u00e1nyt" } } - } + }, + "title": "Tan\u00fas\u00edtv\u00e1ny lej\u00e1rata" } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/zh-Hans.json b/homeassistant/components/cert_expiry/translations/zh-Hans.json index 07affc990a8..201749ae796 100644 --- a/homeassistant/components/cert_expiry/translations/zh-Hans.json +++ b/homeassistant/components/cert_expiry/translations/zh-Hans.json @@ -1,15 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "import_failed": "\u914d\u7f6e\u5bfc\u5165\u5931\u8d25" + }, "error": { - "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6" + "connection_refused": "\u8fde\u63a5\u5230\u4e3b\u673a\u65f6\u88ab\u62d2\u7edd\u8fde\u63a5", + "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6", + "resolve_failed": "\u65e0\u6cd5\u89e3\u6790\u4e3b\u673a" }, "step": { "user": { "data": { - "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d", + "host": "\u4e3b\u673a\u5730\u5740", "name": "\u8bc1\u4e66\u7684\u540d\u79f0", "port": "\u8bc1\u4e66\u7684\u7aef\u53e3" - } + }, + "title": "\u5b9a\u4e49\u8981\u6d4b\u8bd5\u7684\u8bc1\u4e66" } } } diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 7d54d259051..fd0c96c6fbe 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -265,7 +265,7 @@ class CityBikesNetwork: class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" - _attr_unit_of_measurement = "bikes" + _attr_native_unit_of_measurement = "bikes" _attr_icon = "mdi:bike" def __init__(self, network, station_id, entity_id): @@ -281,7 +281,7 @@ class CityBikesStation(SensorEntity): station_data = station break self._attr_name = station_data.get(ATTR_NAME) - self._attr_state = station_data.get(ATTR_FREE_BIKES) + self._attr_native_value = station_data.get(ATTR_FREE_BIKES) self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, 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} diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 3f96dd9e02c..1ba5bbe3a34 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -68,7 +68,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): f"{self._config_entry.unique_id}_{slugify(description.name)}" ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} - self._attr_unit_of_measurement = ( + self._attr_native_unit_of_measurement = ( description.unit_metric if hass.config.units.is_metric else description.unit_imperial @@ -80,7 +80,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): """Return the raw state.""" @property - def state(self) -> str | int | float | None: + def native_value(self) -> str | int | float | None: """Return the state.""" state = self._state if ( diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 7516f32c3e1..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.44.0"], + "requirements": ["hass-nabucasa==0.46.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/cloudflare/translations/zh-Hans.json b/homeassistant/components/cloudflare/translations/zh-Hans.json index 4b0a696e5fc..78429184bad 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hans.json +++ b/homeassistant/components/cloudflare/translations/zh-Hans.json @@ -5,6 +5,11 @@ "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" }, "step": { + "reauth_confirm": { + "data": { + "description": "\u4f7f\u7528\u60a8\u7684 Cloudflare \u5e10\u6237\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002" + } + }, "user": { "data": { "api_token": "API \u5bc6\u7801" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index bd8d94355fd..a4c1062e2c6 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -118,16 +118,17 @@ 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 - def state(self) -> StateType: + def native_value(self) -> StateType: """Return sensor state.""" return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" if self._description.unit_of_measurement: return self._description.unit_of_measurement diff --git a/homeassistant/components/co2signal/translations/es.json b/homeassistant/components/co2signal/translations/es.json new file mode 100644 index 00000000000..071ae642c74 --- /dev/null +++ b/homeassistant/components/co2signal/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "api_ratelimit": "Se ha superado el l\u00edmite de velocidad de la API" + }, + "step": { + "country": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds" + } + }, + "user": { + "data": { + "location": "Obtener datos para" + }, + "description": "Visite https://co2signal.com/ para solicitar un token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/hu.json b/homeassistant/components/co2signal/translations/hu.json new file mode 100644 index 00000000000..00bc19e7b49 --- /dev/null +++ b/homeassistant/components/co2signal/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + }, + "country": { + "data": { + "country_code": "Orsz\u00e1g k\u00f3d" + } + }, + "user": { + "data": { + "api_key": "Hozz\u00e1f\u00e9r\u00e9si token", + "location": "Adatok lek\u00e9rdez\u00e9se a" + }, + "description": "Token k\u00e9r\u00e9s\u00e9hez l\u00e1togasson el a https://co2signal.com/ webhelyre." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/no.json b/homeassistant/components/co2signal/translations/no.json new file mode 100644 index 00000000000..bb56f0c1364 --- /dev/null +++ b/homeassistant/components/co2signal/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "api_ratelimit": "API Ratelimit overskredet", + "unknown": "Uventet feil" + }, + "error": { + "api_ratelimit": "API Ratelimit overskredet", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + } + }, + "country": { + "data": { + "country_code": "Landskode" + } + }, + "user": { + "data": { + "api_key": "Tilgangstoken", + "location": "Hent data for" + }, + "description": "Bes\u00f8k https://co2signal.com/ for \u00e5 be om et token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/zh-Hans.json b/homeassistant/components/co2signal/translations/zh-Hans.json new file mode 100644 index 00000000000..af750541de5 --- /dev/null +++ b/homeassistant/components/co2signal/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u8bbf\u95ee\u4ee4\u724c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 4ea36dad266..5901aeeed9a 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -14,6 +14,8 @@ from . import get_accounts from .const import ( API_ACCOUNT_CURRENCY, API_RATES, + API_RESOURCE_TYPE, + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, @@ -65,7 +67,11 @@ async def validate_options( accounts = await hass.async_add_executor_job(get_accounts, client) - accounts_currencies = [account[API_ACCOUNT_CURRENCY] for account in accounts] + accounts_currencies = [ + account[API_ACCOUNT_CURRENCY] + for account in accounts + if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ] available_rates = await hass.async_add_executor_job(client.get_exchange_rates) if CONF_CURRENCIES in options: for currency in options[CONF_CURRENCIES]: diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index a7ed0b15986..dc2922d1531 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -18,6 +18,8 @@ API_ACCOUNT_NATIVE_BALANCE = "native_balance" API_ACCOUNT_NAME = "name" API_ACCOUNTS_DATA = "data" API_RATES = "rates" +API_RESOURCE_TYPE = "type" +API_TYPE_VAULT = "vault" WALLETS = { "1INCH": "1INCH", diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index c86f21bac1d..d5abb7d66f5 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -12,6 +12,8 @@ from .const import ( API_ACCOUNT_NAME, API_ACCOUNT_NATIVE_BALANCE, API_RATES, + API_RESOURCE_TYPE, + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, @@ -41,7 +43,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] provided_currencies = [ - account[API_ACCOUNT_CURRENCY] for account in instance.accounts + account[API_ACCOUNT_CURRENCY] + for account in instance.accounts + if account[API_RESOURCE_TYPE] != API_TYPE_VAULT ] desired_currencies = [] @@ -82,7 +86,10 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if account[API_ACCOUNT_CURRENCY] == currency: + if ( + account[API_ACCOUNT_CURRENCY] == currency + and account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ): self._name = f"Coinbase {account[API_ACCOUNT_NAME]}" self._id = ( f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" @@ -109,12 +116,12 @@ class AccountSensor(SensorEntity): return self._id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement @@ -135,7 +142,10 @@ class AccountSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: - if account[API_ACCOUNT_CURRENCY] == self._currency: + if ( + account[API_ACCOUNT_CURRENCY] == self._currency + and account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ): self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ API_ACCOUNT_AMOUNT @@ -171,12 +181,12 @@ class ExchangeRateSensor(SensorEntity): return self._id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index 32a69bfe33d..c6f6a1f36f9 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -19,6 +19,13 @@ "options": { "error": { "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "init": { + "data": { + "exchange_base": "Z\u00e1kladn\u00ed m\u011bna pro senzory sm\u011bnn\u00fdch kurz\u016f." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 747049fbd5c..78cf46d717a 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Lommeboksaldoer som skal rapporteres.", + "exchange_base": "Standardvaluta for valutakurssensorer.", "exchange_rate_currencies": "Valutakurser som skal rapporteres." }, "description": "Juster Coinbase-alternativer" diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 5d4ec6eec13..48ec0c46536 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -86,12 +86,12 @@ class ComedHourlyPricingSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 728bc13b76b..a6a625bab99 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -297,7 +297,7 @@ class ComfoConnectSensor(SensorEntity): self.schedule_update_ha_state() @property - def state(self): + def native_value(self): """Return the state of the entity.""" try: return self._ccb.data[self._sensor_id] @@ -325,7 +325,7 @@ class ComfoConnectSensor(SensorEntity): return SENSOR_TYPES[self._sensor_type][ATTR_ICON] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return SENSOR_TYPES[self._sensor_type][ATTR_UNIT] diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 10c5a16f60b..43e05a429b6 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -84,12 +84,12 @@ class CommandSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 35ca07ce522..257c6b4a354 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -107,7 +107,7 @@ class CompensationSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -123,7 +123,7 @@ class CompensationSensor(SensorEntity): return ret @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index d9029dc497f..a6b39e556aa 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -44,8 +44,8 @@ class CheckConfigView(HomeAssistantView): vol.Optional("unit_system"): cv.unit_system, vol.Optional("location_name"): str, vol.Optional("time_zone"): cv.time_zone, - vol.Optional("external_url"): vol.Any(cv.url, None), - vol.Optional("internal_url"): vol.Any(cv.url, None), + vol.Optional("external_url"): vol.Any(cv.url_no_path, None), + vol.Optional("internal_url"): vol.Any(cv.url_no_path, None), vol.Optional("currency"): cv.currency, } ) diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 68cb4fe23a9..5d41eb09a84 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -14,6 +14,16 @@ "host": "IP c\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" } } } diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index bf67763ca6b..d52dba6b4b4 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -1,14 +1,21 @@ { "config": { "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_units": "Nem tal\u00e1lhat\u00f3 HVAC egys\u00e9g a CoolMasterNet gazdag\u00e9pben." }, "step": { "user": { "data": { + "cool": "T\u00e1mogatott a h\u0171t\u00e9si m\u00f3d(ok)", + "dry": "T\u00e1mogassa a p\u00e1r\u00e1tlan\u00edt\u00f3 m\u00f3d(ok)", + "fan_only": "T\u00e1mogaott csak ventil\u00e1tor m\u00f3d(ok)", + "heat": "T\u00e1mogatott f\u0171t\u00e9si m\u00f3d(ok)", + "heat_cool": "T\u00e1mogatott f\u0171t\u00e9si/h\u0171t\u00e9si m\u00f3d(ok)", "host": "Hoszt", "off": "Ki lehet kapcsolni" - } + }, + "title": "\u00c1ll\u00edtsa be a CoolMasterNet kapcsolat r\u00e9szleteit." } } } diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index c855137fcbf..d130e131c8b 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -8,13 +8,14 @@ import coronavirus from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, entity_registry, update_coordinator +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Coronavirus component.""" # Make sure coordinator is initialized. await get_coordinator(hass) diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index b467a5fee12..92fdf232214 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class CoronavirusSensor(CoordinatorEntity, SensorEntity): """Sensor representing corona virus data.""" - _attr_unit_of_measurement = "people" + _attr_native_unit_of_measurement = "people" def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" @@ -53,7 +53,7 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """State of the sensor.""" if self.country == OPTION_WORLDWIDE: sum_cases = 0 diff --git a/homeassistant/components/coronavirus/translations/zh-Hans.json b/homeassistant/components/coronavirus/translations/zh-Hans.json index 5bb92ac1172..6348ac40896 100644 --- a/homeassistant/components/coronavirus/translations/zh-Hans.json +++ b/homeassistant/components/coronavirus/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u6b64\u56fd\u5bb6/\u5730\u533a\u5df2\u914d\u7f6e\u5b8c\u6210\u3002" + "already_configured": "\u6b64\u56fd\u5bb6/\u5730\u533a\u5df2\u914d\u7f6e\u5b8c\u6210\u3002", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "user": { diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 01938344694..c34ea939de7 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -43,12 +43,12 @@ class CpuSpeedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return FREQUENCY_GIGAHERTZ diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 6a3fc7b4215..74d3d9a36a2 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -111,7 +111,7 @@ class CupsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._printer is None: return None @@ -183,7 +183,7 @@ class IPPSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._attributes is None: return None @@ -257,7 +257,7 @@ class MarkerSensor(SensorEntity): return ICON_MARKER @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._attributes is None: return None @@ -265,7 +265,7 @@ class MarkerSensor(SensorEntity): return self._attributes[self._printer]["marker-levels"][self._index] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index f42534f509b..fd3f3b2f8c5 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -65,7 +65,7 @@ class CurrencylayerSensor(SensorEntity): self._state = None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._quote @@ -80,7 +80,7 @@ class CurrencylayerSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 3bfc0a3926c..0defa633387 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -86,7 +86,7 @@ class DaikinSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" raise NotImplementedError @@ -101,7 +101,7 @@ class DaikinSensor(SensorEntity): return self._sensor.get(CONF_ICON) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor[CONF_UNIT_OF_MEASUREMENT] @@ -119,7 +119,7 @@ class DaikinClimateSensor(DaikinSensor): """Representation of a Climate Sensor.""" @property - def state(self): + def native_value(self): """Return the internal state of the sensor.""" if self._device_attribute == ATTR_INSIDE_TEMPERATURE: return self._api.device.inside_temperature @@ -141,7 +141,7 @@ class DaikinPowerSensor(DaikinSensor): """Representation of a power/energy consumption sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._device_attribute == ATTR_TOTAL_POWER: return round(self._api.device.current_total_power_consumption, 2) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 792a95e8ac4..25db56a1624 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -100,12 +100,12 @@ class DanfossAir(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 058969d96f9..e73d9b2e1be 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -574,12 +574,12 @@ class DarkSkySensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @@ -730,7 +730,7 @@ class DarkSkyAlertSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index a67d7181a90..3ff5d087e14 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.4.0"], + "requirements": ["debugpy==1.4.1"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 9282f2d26cc..012e686534f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -17,6 +17,7 @@ from pydeconz.sensor import ( from homeassistant.components.sensor import ( DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -41,7 +42,6 @@ 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 @@ -68,7 +68,7 @@ ICON = { } STATE_CLASS = { - Consumption: STATE_CLASS_MEASUREMENT, + Consumption: STATE_CLASS_TOTAL_INCREASING, Humidity: STATE_CLASS_MEASUREMENT, Pressure: STATE_CLASS_MEASUREMENT, Temperature: STATE_CLASS_MEASUREMENT, @@ -160,10 +160,9 @@ class DeconzSensor(DeconzDevice, SensorEntity): self._attr_device_class = DEVICE_CLASS.get(type(self._device)) self._attr_icon = ICON.get(type(self._device)) 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) + self._attr_native_unit_of_measurement = UNIT_OF_MEASUREMENT.get( + type(self._device) + ) @callback def async_update_callback(self, force_update=False): @@ -173,7 +172,7 @@ class DeconzSensor(DeconzDevice, SensorEntity): super().async_update_callback(force_update=force_update) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.state @@ -217,7 +216,7 @@ class DeconzTemperature(DeconzDevice, SensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS TYPE = DOMAIN @@ -240,7 +239,7 @@ class DeconzTemperature(DeconzDevice, SensorEntity): super().async_update_callback(force_update=force_update) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.secondary_temperature @@ -250,7 +249,7 @@ class DeconzBattery(DeconzDevice, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE TYPE = DOMAIN @@ -284,7 +283,7 @@ class DeconzBattery(DeconzDevice, SensorEntity): return f"{self.serial}-battery" @property - def state(self): + def native_value(self): """Return the state of the battery.""" return self._device.battery diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json index be165a206bf..00e054aecc9 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge er allerede konfigureret", "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", - "no_bridges": "Ingen deConz-bridge fundet", + "no_bridges": "Ingen deConz-bro fundet", "not_deconz_bridge": "Ikke en deCONZ-bro", "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 84493ccb9f6..bc003a279e8 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -26,12 +26,18 @@ "host": "Hoszt", "port": "Port" } + }, + "user": { + "data": { + "host": "V\u00e1lassza ki a felfedezett deCONZ \u00e1tj\u00e1r\u00f3t" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Mindk\u00e9t gomb", + "bottom_buttons": "Als\u00f3 gombok", "button_1": "Els\u0151 gomb", "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", @@ -52,6 +58,7 @@ "side_4": "4. oldal", "side_5": "5. oldal", "side_6": "6. oldal", + "top_buttons": "Fels\u0151 gombok", "turn_off": "Kikapcsolva", "turn_on": "Bekapcsolva" }, @@ -63,6 +70,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", + "remote_button_rotated_fast": "A gomb gyorsan elfordult: \"{subtype}\"", "remote_button_rotation_stopped": "A (z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", "remote_button_short_release": "\"{subtype}\" gomb elengedve", @@ -93,7 +101,8 @@ "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se", "allow_new_devices": "Enged\u00e9lyezze az \u00faj eszk\u00f6z\u00f6k automatikus hozz\u00e1ad\u00e1s\u00e1t" }, - "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa", + "title": "deCONZ opci\u00f3k" } } } diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index b105ff5ff7b..395c2d93dff 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -112,7 +112,7 @@ class DeLijnPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 0c79e6f835e..63c9645dac4 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -87,7 +87,7 @@ class DelugeSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -97,7 +97,7 @@ class DelugeSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 56726bba8b7..572a5bf331e 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -1,4 +1,6 @@ """Demo camera platform that has a fake camera.""" +from __future__ import annotations + from pathlib import Path from homeassistant.components.camera import SUPPORT_ON_OFF, Camera @@ -6,7 +8,12 @@ from homeassistant.components.camera import SUPPORT_ON_OFF, Camera async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo camera platform.""" - async_add_entities([DemoCamera("Demo camera")]) + async_add_entities( + [ + DemoCamera("Demo camera", "image/jpg"), + DemoCamera("Demo camera png", "image/png"), + ] + ) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -17,18 +24,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoCamera(Camera): """The representation of a Demo camera.""" - def __init__(self, name): + def __init__(self, name, content_type): """Initialize demo camera component.""" super().__init__() self._name = name + self.content_type = content_type self._motion_status = False self.is_streaming = True self._images_index = 0 - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes: """Return a faked still image response.""" self._images_index = (self._images_index + 1) % 4 - image_path = Path(__file__).parent / f"demo_{self._images_index}.jpg" + ext = "jpg" if self.content_type == "image/jpg" else "png" + image_path = Path(__file__).parent / f"demo_{self._images_index}.{ext}" return await self.hass.async_add_executor_job(image_path.read_bytes) diff --git a/homeassistant/components/demo/demo_0.png b/homeassistant/components/demo/demo_0.png new file mode 100644 index 00000000000..f45852e3b20 Binary files /dev/null and b/homeassistant/components/demo/demo_0.png differ diff --git a/homeassistant/components/demo/demo_1.png b/homeassistant/components/demo/demo_1.png new file mode 100644 index 00000000000..0a2131a773e Binary files /dev/null and b/homeassistant/components/demo/demo_1.png differ diff --git a/homeassistant/components/demo/demo_2.png b/homeassistant/components/demo/demo_2.png new file mode 100644 index 00000000000..97a8e49025d Binary files /dev/null and b/homeassistant/components/demo/demo_2.png differ diff --git a/homeassistant/components/demo/demo_3.png b/homeassistant/components/demo/demo_3.png new file mode 100644 index 00000000000..0a2131a773e Binary files /dev/null and b/homeassistant/components/demo/demo_3.png differ diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 7eabf9bea2d..af61c0f6111 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -97,3 +97,4 @@ class DemoLock(LockEntity): """Flag supported features.""" if self._openable: return SUPPORT_OPEN + return 0 diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 488c34be983..21ec8e1d391 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -120,10 +120,10 @@ class DemoSensor(SensorEntity): """Initialize the sensor.""" self._attr_device_class = device_class self._attr_name = name - self._attr_state = state + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state self._attr_state_class = state_class self._attr_unique_id = unique_id - self._attr_unit_of_measurement = unit_of_measurement self._attr_device_info = { "identifiers": {(DOMAIN, unique_id)}, diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 0f8f1673d43..3bfe095189a 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,9 +1,16 @@ { "options": { "step": { + "init": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + } + }, "options_1": { "data": { "bool": "Opcion\u00e1lis logikai \u00e9rt\u00e9k", + "constant": "\u00c1lland\u00f3", "int": "Numerikus bemenet" } }, diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index e6727d3c29f..43ee362d65a 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -3,17 +3,32 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet" + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", + "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, felfedezett gy\u00e1rt\u00f3 nem egyezik", + "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, "error": { "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" + }, + "select": { + "data": { + "select_host": "Vev\u0151 IP-c\u00edme" + }, + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha tov\u00e1bbi vev\u0151k\u00e9sz\u00fcl\u00e9keket szeretne csatlakoztatni", + "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt vev\u0151t" + }, "user": { "data": { "host": "IP c\u00edm" - } + }, + "description": "Csatlakozzon a vev\u0151h\u00f6z, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" } } }, @@ -21,8 +36,13 @@ "step": { "init": { "data": { - "update_audyssey": "Friss\u00edtse az Audyssey be\u00e1ll\u00edt\u00e1sait" - } + "show_all_sources": "Az \u00f6sszes forr\u00e1s megjelen\u00edt\u00e9se", + "update_audyssey": "Friss\u00edtse az Audyssey be\u00e1ll\u00edt\u00e1sait", + "zone2": "\u00c1ll\u00edtsa be a 2. z\u00f3n\u00e1t", + "zone3": "\u00c1ll\u00edtsa be a 3. z\u00f3n\u00e1t" + }, + "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" } } } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index c8b639a1db1..45f5db57a90 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -196,12 +196,12 @@ class DerivativeSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 33fd9a8224f..34711a9a2d7 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -59,7 +59,7 @@ class DeutscheBahnSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the departure time of the next train.""" return self._state diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 93b0b9a4a9d..945774da0b4 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import MutableMapping +from collections.abc import Iterable, Mapping from functools import wraps from types import ModuleType from typing import Any @@ -13,9 +13,12 @@ import voluptuous_serialize from homeassistant.components import websocket_api from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.loader import IntegrationNotFound +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) +from homeassistant.loader import IntegrationNotFound, bind_hass from homeassistant.requirements import async_get_integration_with_requirements from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig @@ -49,6 +52,16 @@ TYPES = { } +@bind_hass +async def async_get_device_automations( + hass: HomeAssistant, + automation_type: str, + device_ids: Iterable[str] | None = None, +) -> Mapping[str, Any]: + """Return all the device automations for a type optionally limited to specific device ids.""" + return await _async_get_device_automations(hass, automation_type, device_ids) + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -96,7 +109,7 @@ async def async_get_device_automation_platform( async def _async_get_device_automations_from_domain( - hass, domain, automation_type, device_id + hass, domain, automation_type, device_ids, return_exceptions ): """List device automations.""" try: @@ -104,48 +117,67 @@ async def _async_get_device_automations_from_domain( hass, domain, automation_type ) except InvalidDeviceAutomationConfig: - return None + return {} function_name = TYPES[automation_type][1] - return await getattr(platform, function_name)(hass, device_id) - - -async def _async_get_device_automations(hass, automation_type, device_id): - """List device automations.""" - device_registry, entity_registry = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), + return await asyncio.gather( + *( + getattr(platform, function_name)(hass, device_id) + for device_id in device_ids + ), + return_exceptions=return_exceptions, ) - domains = set() - automations: list[MutableMapping[str, Any]] = [] - device = device_registry.async_get(device_id) - if device is None: - raise DeviceNotFound +async def _async_get_device_automations( + hass: HomeAssistant, automation_type: str, device_ids: Iterable[str] | None +) -> Mapping[str, list[dict[str, Any]]]: + """List device automations.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + domain_devices: dict[str, set[str]] = {} + device_entities_domains: dict[str, set[str]] = {} + match_device_ids = set(device_ids or device_registry.devices) + combined_results: dict[str, list[dict[str, Any]]] = {} - for entry_id in device.config_entries: - config_entry = hass.config_entries.async_get_entry(entry_id) - domains.add(config_entry.domain) + for entry in entity_registry.entities.values(): + if not entry.disabled_by and entry.device_id in match_device_ids: + device_entities_domains.setdefault(entry.device_id, set()).add(entry.domain) - entity_entries = async_entries_for_device(entity_registry, device_id) - for entity_entry in entity_entries: - domains.add(entity_entry.domain) + for device_id in match_device_ids: + combined_results[device_id] = [] + device = device_registry.async_get(device_id) + if device is None: + raise DeviceNotFound + for entry_id in device.config_entries: + if config_entry := hass.config_entries.async_get_entry(entry_id): + domain_devices.setdefault(config_entry.domain, set()).add(device_id) + for domain in device_entities_domains.get(device_id, []): + domain_devices.setdefault(domain, set()).add(device_id) - device_automations = await asyncio.gather( + # If specific device ids were requested, we allow + # InvalidDeviceAutomationConfig to be thrown, otherwise we skip + # devices that do not have valid triggers + return_exceptions = not bool(device_ids) + + for domain_results in await asyncio.gather( *( _async_get_device_automations_from_domain( - hass, domain, automation_type, device_id + hass, domain, automation_type, domain_device_ids, return_exceptions ) - for domain in domains + for domain, domain_device_ids in domain_devices.items() ) - ) - for device_automation in device_automations: - if device_automation is not None: - automations.extend(device_automation) + ): + for device_results in domain_results: + if device_results is None or isinstance( + device_results, InvalidDeviceAutomationConfig + ): + continue + for automation in device_results: + combined_results[automation["device_id"]].append(automation) - return automations + return combined_results async def _async_get_device_automation_capabilities(hass, automation_type, automation): @@ -207,7 +239,9 @@ def handle_device_errors(func): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = await _async_get_device_automations(hass, "action", device_id) + actions = (await _async_get_device_automations(hass, "action", [device_id])).get( + device_id + ) connection.send_result(msg["id"], actions) @@ -222,7 +256,9 @@ async def websocket_device_automation_list_actions(hass, connection, msg): async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] - conditions = await _async_get_device_automations(hass, "condition", device_id) + conditions = ( + await _async_get_device_automations(hass, "condition", [device_id]) + ).get(device_id) connection.send_result(msg["id"], conditions) @@ -237,7 +273,9 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await _async_get_device_automations(hass, "trigger", device_id) + triggers = (await _async_get_device_automations(hass, "trigger", [device_id])).get( + device_id + ) connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 781799cbf37..03f850579be 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -45,7 +45,6 @@ class DevoloDeviceEntity(Entity): self.subscriber: Subscriber | None = None self.sync_callback = self._sync self._value: int - self._unit = "" async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 7cb8cc8e837..61c3e9a5c19 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -78,7 +78,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._value @@ -106,7 +106,7 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): self._attr_device_class = DEVICE_CLASS_MAPPING.get( self._multi_level_sensor_property.sensor_type ) - self._attr_unit_of_measurement = self._multi_level_sensor_property.unit + self._attr_native_unit_of_measurement = self._multi_level_sensor_property.unit self._value = self._multi_level_sensor_property.value @@ -132,7 +132,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): ) self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") - self._attr_unit_of_measurement = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._value = device_instance.battery_level @@ -157,15 +157,12 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._sensor_type = consumption self._attr_device_class = DEVICE_CLASS_MAPPING.get(consumption) - self._attr_unit_of_measurement = getattr( + self._attr_native_unit_of_measurement = getattr( device_instance.consumption_property[element_uid], f"{consumption}_unit" ) if consumption == "total": - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_last_reset = device_instance.consumption_property[ - element_uid - ].total_since + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._value = getattr( device_instance.consumption_property[element_uid], consumption @@ -180,15 +177,11 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): def _sync(self, message: tuple) -> None: """Update the consumption sensor state.""" - if message[0] == self._attr_unique_id and message[2] != "total_since": + if message[0] == self._attr_unique_id: self._value = getattr( self._device_instance.consumption_property[self._attr_unique_id], self._sensor_type, ) - elif message[0] == self._attr_unique_id and message[2] == "total_since": - self._attr_last_reset = self._device_instance.consumption_property[ - self._attr_unique_id - ].total_since else: self._generic_message(message) self.schedule_update_ha_state() diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index 730a1824e1a..316f36e3630 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -42,12 +42,12 @@ class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): return GLUCOSE_VALUE_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the device.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return getattr(self.coordinator.data, self._attribute_unit_of_measurement) @@ -82,7 +82,7 @@ class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity): return GLUCOSE_TREND_ICON[0] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return self.coordinator.data.trend_description diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json index 45f38b22a84..039eb56f8f0 100644 --- a/homeassistant/components/dexcom/translations/hu.json +++ b/homeassistant/components/dexcom/translations/hu.json @@ -14,7 +14,9 @@ "password": "Jelsz\u00f3", "server": "Szerver", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Adja meg a Dexcom Share hiteles\u00edt\u0151 adatait", + "title": "Dexcom integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 7003038593b..1a49667bad8 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -41,6 +41,7 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_dhcp from homeassistant.util.network import is_invalid, is_link_local, is_loopback @@ -58,7 +59,7 @@ SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" async def _initialize(_): diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 1300c165b37..d81d12f33cf 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -134,12 +134,12 @@ class DHTSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json index 0309eb35881..3e0a7d5cb57 100644 --- a/homeassistant/components/directv/translations/hu.json +++ b/homeassistant/components/directv/translations/hu.json @@ -7,7 +7,15 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + }, "user": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 81beec0e60e..3d90956a2b5 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -105,7 +105,7 @@ class DiscogsSensor(SensorEntity): return f"{self._name} {SENSORS[self._type]['name']}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -115,7 +115,7 @@ class DiscogsSensor(SensorEntity): return SENSORS[self._type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return SENSORS[self._type]["unit_of_measurement"] diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e9ac437fe46..67d9713628a 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.19.1"], + "requirements": ["async-upnp-client==0.20.0"], "dependencies": ["network"], "codeowners": [], "iot_class": "local_push" diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 2fb0e30da90..a429d336379 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -79,6 +79,6 @@ class WanIpSensor(SensorEntity): response = None if response: - self._attr_state = response[0].host + self._attr_native_value = response[0].host else: - self._attr_state = None + self._attr_native_value = None diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d5964d5aea0..07366ad1a9a 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util, slugify from .const import ( @@ -58,7 +59,7 @@ DEVICE_SCHEMA = vol.Schema( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 53fcdbcee70..16606156314 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -1,4 +1,6 @@ """Support for viewing the camera feed from a DoorBird video doorbell.""" +from __future__ import annotations + import asyncio import datetime import logging @@ -112,7 +114,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera): """Get the name of the camera.""" return self._name - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Pull a still image from the camera.""" now = dt_util.utcnow() diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index 3f74783b7ac..cb4c46e699a 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "link_local_address": "A linkek helyi c\u00edmei nem t\u00e1mogatottak", + "not_doorbird_device": "Ez az eszk\u00f6z nem DoorBird" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -16,7 +18,18 @@ "name": "Eszk\u00f6z neve", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a DoorBird-hez" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Vessz\u0151vel elv\u00e1lasztott esem\u00e9nyek list\u00e1ja." + }, + "description": "Adjon hozz\u00e1 vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden k\u00f6vetni k\u00edv\u00e1nt esem\u00e9nyhez. Miut\u00e1n itt megadta \u0151ket, haszn\u00e1lja a DoorBird alkalmaz\u00e1st, hogy hozz\u00e1rendelje \u0151ket egy adott esem\u00e9nyhez. Tekintse meg a dokument\u00e1ci\u00f3t a https://www.home-assistant.io/integrations/doorbird/#events c\u00edmen. P\u00e9lda: valaki_pr\u00e9selt_gomb, mozg\u00e1s" } } } diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index e7b3dbdd363..46f4c34cc31 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -89,7 +89,7 @@ class DovadoSensor(SensorEntity): return f"{self._data.name} {SENSORS[self._sensor][1]}" @property - def state(self): + def native_value(self): """Return the sensor state.""" return self._state @@ -99,7 +99,7 @@ class DovadoSensor(SensorEntity): return SENSORS[self._sensor][3] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSORS[self._sensor][2] diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 72e854fe43a..9670aab21cf 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -28,6 +28,7 @@ from .const import ( CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, + DSMR_VERSIONS, LOGGER, ) @@ -70,6 +71,10 @@ class DSMRConnection: if self._equipment_identifier in telegram: self._telegram = telegram transport.close() + # Swedish meters have no equipment identifier + if self._dsmr_version == "5S" and obis_ref.P1_MESSAGE_TIMESTAMP in telegram: + self._telegram = telegram + transport.close() if self._host is None: reader_factory = partial( @@ -119,7 +124,7 @@ async def _validate_dsmr_connection( equipment_identifier_gas = conn.equipment_identifier_gas() # Check only for equipment identifier in case no gas meter is connected - if equipment_identifier is None: + if equipment_identifier is None and data[CONF_DSMR_VERSION] != "5S": raise CannotCommunicate return { @@ -203,7 +208,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_HOST): str, vol.Required(CONF_PORT): int, - vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS), } ) return self.async_show_form( @@ -247,7 +252,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema( { vol.Required(CONF_PORT): vol.In(list_of_ports), - vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS), } ) return self.async_show_form( @@ -288,8 +293,9 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = {**data, **info} - await self.async_set_unique_id(info[CONF_SERIAL_ID]) - self._abort_if_unique_id_configured() + if info[CONF_SERIAL_ID]: + await self.async_set_unique_id(info[CONF_SERIAL_ID]) + self._abort_if_unique_id_configured() except CannotConnect: errors["base"] = "cannot_connect" except CannotCommunicate: @@ -316,8 +322,9 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): name = f"{host}:{port}" if host is not None else port data = {**import_config, **info} - await self.async_set_unique_id(info[CONF_SERIAL_ID]) - self._abort_if_unique_id_configured(data) + if info[CONF_SERIAL_ID]: + await self.async_set_unique_id(info[CONF_SERIAL_ID]) + self._abort_if_unique_id_configured(data) return self.async_create_entry(title=name, data=data) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index a5e51816183..ba90fa9b697 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -5,14 +5,17 @@ import logging from dsmr_parser import obis_references -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ) -from homeassistant.util import dt from .models import DSMRSensorEntityDescription @@ -41,6 +44,8 @@ DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" DEVICE_NAME_GAS = "Gas Meter" +DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S"} + SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_USAGE, @@ -59,39 +64,40 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_ACTIVE_TARIFF, name="Power Tariff", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_USED_TARIFF_1, name="Energy Consumption (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=DEVICE_CLASS_ENERGY, force_update=True, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_USED_TARIFF_2, name="Energy Consumption (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, name="Energy Production (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, name="Energy Production (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, @@ -138,45 +144,53 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.SHORT_POWER_FAILURE_COUNT, name="Short Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", ), DSMRSensorEntityDescription( key=obis_references.LONG_POWER_FAILURE_COUNT, name="Long Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L1_COUNT, name="Voltage Sags Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L2_COUNT, name="Voltage Sags Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L3_COUNT, name="Voltage Sags Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L1_COUNT, name="Voltage Swells Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L2_COUNT, name="Voltage Swells Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L3_COUNT, name="Voltage Swells Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), @@ -228,8 +242,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, @@ -237,8 +250,23 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, + name="Energy Consumption (total)", + dsmr_versions={"5S"}, + force_update=True, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + name="Energy Production (total)", + dsmr_versions={"5S"}, + force_update=True, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_IMPORTED_TOTAL, @@ -246,8 +274,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.HOURLY_GAS_METER_READING, @@ -255,9 +282,8 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, - icon="mdi:fire", - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.BELGIUM_HOURLY_GAS_METER_READING, @@ -265,9 +291,8 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5B"}, is_gas=True, force_update=True, - icon="mdi:fire", - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.GAS_METER_READING, @@ -275,8 +300,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2"}, is_gas=True, force_update=True, - icon="mdi:fire", - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), ) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index df738724ac0..fbbfac55959 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.29"], + "requirements": ["dsmr_parser==0.30"], "codeowners": ["@Robbie1221", "@frenck"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index faff62ddeb4..1b38b2695ec 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -16,7 +16,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + VOLUME_CUBIC_METERS, +) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,6 +44,7 @@ from .const import ( DEVICE_NAME_ENERGY, DEVICE_NAME_GAS, DOMAIN, + DSMR_VERSIONS, LOGGER, SENSORS, ) @@ -49,13 +55,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(["5L", "5B", "5", "4", "2.2"]) + cv.string, vol.In(DSMR_VERSIONS) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), } ) +UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} + async def async_setup_platform( hass: HomeAssistant, @@ -111,7 +119,7 @@ async def async_setup_entry( create_tcp_dsmr_reader, entry.data[CONF_HOST], entry.data[CONF_PORT], - entry.data[CONF_DSMR_VERSION], + dsmr_version, update_entities_telegram, loop=hass.loop, keep_alive_interval=60, @@ -120,7 +128,7 @@ async def async_setup_entry( reader_factory = partial( create_dsmr_reader, entry.data[CONF_PORT], - entry.data[CONF_DSMR_VERSION], + dsmr_version, update_entities_telegram, loop=hass.loop, ) @@ -210,6 +218,8 @@ class DSMREntity(SensorEntity): if entity_description.is_gas: device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS + if device_serial is None: + device_serial = entry.entry_id self._attr_device_info = { "identifiers": {(DOMAIN, device_serial)}, @@ -238,7 +248,7 @@ class DSMREntity(SensorEntity): return attr @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" value = self.get_dsmr_object_attr("value") if value is None: @@ -258,9 +268,12 @@ class DSMREntity(SensorEntity): return None @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return self.get_dsmr_object_attr("unit") + unit_of_measurement = self.get_dsmr_object_attr("unit") + if unit_of_measurement in UNIT_CONVERSION: + return UNIT_CONVERSION[unit_of_measurement] + return unit_of_measurement @staticmethod def translate_tariff(value: str, dsmr_version: str) -> str | None: diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 1a46f86132b..533b2f0dd38 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -6,12 +6,14 @@ from typing import Callable from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntityDescription, ) from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, @@ -21,7 +23,6 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) -from homeassistant.util import dt as dt_util def dsmr_transform(value): @@ -50,46 +51,42 @@ SENSORS: tuple[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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_delivered", name="Current power usage", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_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, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -97,7 +94,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -105,7 +102,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -113,7 +110,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -121,7 +118,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -129,7 +126,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -137,7 +134,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -145,16 +142,15 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( 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), + native_unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -162,7 +158,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current voltage L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -170,7 +166,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current voltage L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -178,7 +174,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -186,7 +182,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -194,7 +190,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -206,16 +202,15 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( 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), + device_class=DEVICE_CLASS_GAS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - icon="mdi:fire", - unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -228,121 +223,115 @@ SENSORS: tuple[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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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), + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", name="Low tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_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, + native_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, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/gas", name="Gas usage", icon="mdi:counter", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_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, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/total_cost", name="Total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/meter-stats/dsmr_version", @@ -415,156 +404,156 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/current-month/electricity1", name="Current month low tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_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, + native_unit_of_measurement=CURRENCY_EURO, ), ) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 39356db46b5..84947ec41f1 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -33,9 +33,9 @@ class DSMRSensor(SensorEntity): def message_received(message): """Handle new MQTT messages.""" if self.entity_description.state is not None: - self._attr_state = self.entity_description.state(message.payload) + self._attr_native_value = self.entity_description.state(message.payload) else: - self._attr_state = message.payload + self._attr_native_value = message.payload self.async_write_ha_state() diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 4e095955818..5b08e8e142c 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -66,12 +66,12 @@ class DteEnergyBridgeSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index dbe1d10b553..b7daf661e63 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -82,7 +82,7 @@ class DublinPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -105,7 +105,7 @@ class DublinPublicTransportSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index cf0b593d546..148a6fde0d0 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -13,6 +13,7 @@ "data": { "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Dune HD integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/dunehd \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva.", "title": "Dune HD" } } diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 428ed3ab427..2668e573b7c 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -112,7 +112,7 @@ class DwdWeatherWarningsSensor(SensorEntity): self._attr_name = f"{name} {description.name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.entity_description.key == CURRENT_WARNING_SENSOR: return self._api.api.current_warning_level diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index f1243cd5407..3d980b34d00 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -73,12 +73,12 @@ class DweetSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 7dc3d86afe6..49e742519fd 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, C from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow from .bridge import DynaliteBridge @@ -179,7 +180,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Dynalite platform.""" conf = config.get(DOMAIN) LOGGER.debug("Setting up dynalite component config = %s", conf) diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index cff4b8f5501..be83a7e4373 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -129,7 +129,7 @@ class DysonSensor(DysonEntity, SensorEntity): return f"{self._device.serial}-{self._sensor_type}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -152,7 +152,7 @@ class DysonFilterLifeSensor(DysonSensor): super().__init__(device, "filter_life") @property - def state(self): + def native_value(self): """Return filter life in hours.""" return int(self._device.state.filter_life) @@ -165,7 +165,7 @@ class DysonCarbonFilterLifeSensor(DysonSensor): super().__init__(device, "carbon_filter_state") @property - def state(self): + def native_value(self): """Return filter life remaining in percent.""" return int(self._device.state.carbon_filter_state) @@ -178,7 +178,7 @@ class DysonHepaFilterLifeSensor(DysonSensor): super().__init__(device, f"{filter_type}_filter_state") @property - def state(self): + def native_value(self): """Return filter life remaining in percent.""" return int(self._device.state.hepa_filter_state) @@ -191,7 +191,7 @@ class DysonDustSensor(DysonSensor): super().__init__(device, "dust") @property - def state(self): + def native_value(self): """Return Dust value.""" return self._device.environmental_state.dust @@ -204,7 +204,7 @@ class DysonHumiditySensor(DysonSensor): super().__init__(device, "humidity") @property - def state(self): + def native_value(self): """Return Humidity value.""" if self._device.environmental_state.humidity == 0: return STATE_OFF @@ -220,7 +220,7 @@ class DysonTemperatureSensor(DysonSensor): self._unit = unit @property - def state(self): + def native_value(self): """Return Temperature value.""" temperature_kelvin = self._device.environmental_state.temperature if temperature_kelvin == 0: @@ -230,7 +230,7 @@ class DysonTemperatureSensor(DysonSensor): return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @@ -243,6 +243,6 @@ class DysonAirQualitySensor(DysonSensor): super().__init__(device, "air_quality") @property - def state(self): + def native_value(self): """Return Air Quality value.""" return int(self._device.environmental_state.volatil_organic_compounds) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index b3d726f9cd3..bc2158e4db8 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -149,7 +149,7 @@ class Measurement(CoordinatorEntity, SensorEntity): return True @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return units for the sensor.""" measure = self.coordinator.data["measures"][self.key] if "unit" not in measure: @@ -162,6 +162,6 @@ class Measurement(CoordinatorEntity, SensorEntity): return {ATTR_ATTRIBUTION: self.attribution} @property - def state(self): + def native_value(self): """Return the current sensor value.""" return self.coordinator.data["measures"][self.key]["latestReading"]["value"] diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json index 38863029f12..820958e4e6e 100644 --- a/homeassistant/components/eafm/translations/hu.json +++ b/homeassistant/components/eafm/translations/hu.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_stations": "Nem tal\u00e1lhat\u00f3 \u00e1rv\u00edzfigyel\u0151 \u00e1llom\u00e1s." }, "step": { "user": { "data": { "station": "\u00c1llom\u00e1s" - } + }, + "description": "V\u00e1lassza ki a figyelni k\u00edv\u00e1nt \u00e1llom\u00e1st", + "title": "\u00c1rv\u00edzfigyel\u0151 \u00e1llom\u00e1s nyomon k\u00f6vet\u00e9se" } } } diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index e27c6fe0772..3c43dd36130 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -45,87 +45,89 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="usage", name="Usage", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", ), SensorEntityDescription( key="balance", name="Balance", - unit_of_measurement=PRICE, + native_unit_of_measurement=PRICE, icon="mdi:cash-usd", ), SensorEntityDescription( key="limit", name="Data limit", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="days_left", name="Days left", - unit_of_measurement=TIME_DAYS, + native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-today", ), SensorEntityDescription( key="before_offpeak_download", name="Download before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="before_offpeak_upload", name="Upload before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="before_offpeak_total", name="Total before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="offpeak_download", name="Offpeak download", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="offpeak_upload", name="Offpeak Upload", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="offpeak_total", name="Offpeak Total", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="download", name="Download", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="upload", name="Upload", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="total", name="Total", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), ) +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, @@ -177,7 +179,7 @@ class EBoxSensor(SensorEntity): """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() if self.entity_description.key in self.ebox_data.data: - self._attr_state = round( + self._attr_native_value = round( self.ebox_data.data[self.entity_description.key], 2 ) 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": [ diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index abd9620130d..dcfd4ec7eef 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -56,7 +56,7 @@ class EbusdSensor(SensorEntity): return f"{self._client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -94,7 +94,7 @@ class EbusdSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index 9a2fbdd9b87..d9689631280 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -33,7 +33,7 @@ class EcoalTempSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -43,7 +43,7 @@ class EcoalTempSensor(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return TEMP_CELSIUS diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 97f9fe6eae0..eb72f667b5f 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -107,7 +107,7 @@ class EcobeeSensor(SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._state in ( ECOBEE_STATE_CALIBRATING, @@ -122,7 +122,7 @@ class EcobeeSensor(SensorEntity): return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 0dfe8df7fb3..bbcf54003e8 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -82,7 +82,7 @@ class EcoNetSensor(EcoNetEntity, SensorEntity): self._device_name = device_name @property - def state(self): + def native_value(self): """Return sensors state.""" value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name]) if isinstance(value, float): @@ -90,7 +90,7 @@ class EcoNetSensor(EcoNetEntity, SensorEntity): return value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name] if self._device_name == POWER_USAGE_TODAY: diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 9adb7665753..1eee0b47272 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -114,7 +114,7 @@ class EddystoneTemp(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.temperature @@ -124,7 +124,7 @@ class EddystoneTemp(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TEMP_CELSIUS diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index a00f77efa0b..407f5902198 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -301,7 +301,7 @@ class EDL21Entity(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the value of the last received telegram.""" return self._telegram.get("value") @@ -315,7 +315,7 @@ class EDL21Entity(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._telegram.get("unit") diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 6e2ac1c01c7..391aca7b4af 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -120,12 +120,12 @@ class EfergySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 01413ceaec0..df0d7882491 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -101,12 +101,12 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PERCENTAGE @@ -157,12 +157,12 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if ( "current_sleep" in self._sensor @@ -316,7 +316,7 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -333,7 +333,7 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): self._state = None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._units == "si": return TEMP_CELSIUS diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json index ef6404bd92d..0cd9f2589b8 100644 --- a/homeassistant/components/elgato/translations/hu.json +++ b/homeassistant/components/elgato/translations/hu.json @@ -13,7 +13,12 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "description": "\u00c1ll\u00edtsa be az Elgato Light-ot, hogy integr\u00e1lhat\u00f3 legyen az HomeAssistantba." + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni a \"{serial_number}\" sorozatsz\u00e1m\u00fa Elgato Light-ot az HomeAssistanthoz?", + "title": "Felfedezett Elgato Light eszk\u00f6z(\u00f6k)" } } } diff --git a/homeassistant/components/elgato/translations/zh-Hans.json b/homeassistant/components/elgato/translations/zh-Hans.json index 254f6df9327..94813c444eb 100644 --- a/homeassistant/components/elgato/translations/zh-Hans.json +++ b/homeassistant/components/elgato/translations/zh-Hans.json @@ -1,10 +1,25 @@ { "config": { "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "flow_title": "{serial_number}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 Elgato Light \u4ee5\u4e0e Home Assistant \u96c6\u6210\u3002" + }, + "zeroconf_confirm": { + "description": "\u60a8\u60f3\u5c06\u5e8f\u5217\u53f7\u4e3a `{serial_number}` \u7684 Elgato Light \u6dfb\u52a0\u5230 Home Assistant \u5417\uff1f", + "title": "\u53d1\u73b0 Elgato Light \u88c5\u7f6e" + } } } } \ No newline at end of file diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 253913b3779..ecd6e4ad4bb 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -78,12 +78,12 @@ class EliqSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return UNIT_OF_MEASUREMENT @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 8f26af545b7..30fe87103c7 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -77,7 +77,7 @@ class ElkSensor(ElkAttachedEntity, SensorEntity): self._state = None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -127,7 +127,7 @@ class ElkKeypad(ElkSensor): return self._temperature_unit @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._temperature_unit @@ -250,7 +250,7 @@ class ElkZone(ElkSensor): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json index 83862dfb75f..ff6445f0b72 100644 --- a/homeassistant/components/elkm1/translations/hu.json +++ b/homeassistant/components/elkm1/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Az ElkM1 ezzel a c\u00edmmel m\u00e1r konfigur\u00e1lva van", + "already_configured": "Az ezzel az el\u0151taggal rendelkez\u0151 ElkM1 m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", @@ -8,10 +12,15 @@ "step": { "user": { "data": { + "address": "Az IP-c\u00edm vagy tartom\u00e1ny vagy soros port, ha soros kapcsolaton kereszt\u00fcl csatlakozik.", "password": "Jelsz\u00f3", + "prefix": "Egyedi el\u0151tag (hagyja \u00fcresen, ha csak egy ElkM1 van).", "protocol": "Protokoll", + "temperature_unit": "Az ElkM1 h\u0151m\u00e9rs\u00e9kleti egys\u00e9g haszn\u00e1lja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A c\u00edmsornak a \u201ebiztons\u00e1gos\u201d \u00e9s a \u201enem biztons\u00e1gos\u201d \u201ec\u00edm [: port]\u201d form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '192.168.1.1'. A port opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 2101 \u201enem biztons\u00e1gos\u201d \u00e9s 2601 \u201ebiztons\u00e1gos\u201d. A soros protokollhoz a c\u00edmnek 'tty [: baud]' form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '/dev/ttyS1'. A baud opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 115200.", + "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" } } } diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index bfc86db387e..5180275b528 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -162,12 +162,12 @@ class EmonCmsSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 69c8b907b72..91263db5127 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp_client.async_get_clientsession(hass) emonitor = Emonitor(entry.data[CONF_HOST], session) - coordinator = DataUpdateCoordinator( + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( hass, _LOGGER, name=entry.title, diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 1dca3f2d89d..1d699b42473 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -38,7 +38,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Representation of an Emonitor power sensor entity.""" _attr_device_class = DEVICE_CLASS_POWER - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: """Initialize the channel sensor.""" @@ -73,7 +73,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): return attr_val @property - def state(self) -> StateType: + def native_value(self) -> StateType: """State of the sensor.""" return self._paired_attr("inst_power") diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index b9dc79e25cc..d513669cd00 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.template import Template, is_template_string +from homeassistant.helpers.typing import ConfigType from .const import CONF_POWER, CONF_POWER_ENTITY, DOMAIN @@ -48,7 +49,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the emulated_kasa component.""" conf = config.get(DOMAIN) if not conf: diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 419a34db98c..bb3ac2082f8 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.9.0"], + "requirements": ["sense_energy==0.9.2"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/emulated_roku/translations/lt.json b/homeassistant/components/emulated_roku/translations/lt.json new file mode 100644 index 00000000000..8ae517ecfbe --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host_ip": "Hosto IP adresas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index c053dea4741..1cea20564b4 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -79,7 +79,34 @@ class SolarSourceType(TypedDict): config_entry_solar_forecast: list[str] | None -SourceType = Union[GridSourceType, SolarSourceType] +class BatterySourceType(TypedDict): + """Dictionary holding the source of battery storage.""" + + type: Literal["battery"] + + stat_energy_from: str + stat_energy_to: str + + +class GasSourceType(TypedDict): + """Dictionary holding the source of gas storage.""" + + type: Literal["gas"] + + stat_energy_from: str + + # statistic_id of costs ($) incurred from the energy meter + # If set to None and entity_energy_from and entity_energy_price are configured, + # an EnergyCostSensor will be automatically created + stat_cost: str | None + + # Used to generate costs if stat_cost is set to None + entity_energy_from: str | None # entity_id of an gas meter (m³), entity_id of the gas meter for stat_energy_from + entity_energy_price: str | None # entity_id of an entity providing price ($/m³) + number_energy_price: float | None # Price for energy ($/m³) + + +SourceType = Union[GridSourceType, SolarSourceType, BatterySourceType, GasSourceType] class DeviceConsumption(TypedDict): @@ -177,6 +204,23 @@ SOLAR_SOURCE_SCHEMA = vol.Schema( vol.Optional("config_entry_solar_forecast"): vol.Any([str], None), } ) +BATTERY_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "battery", + vol.Required("stat_energy_from"): str, + vol.Required("stat_energy_to"): str, + } +) +GAS_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "gas", + vol.Required("stat_energy_from"): str, + vol.Optional("stat_cost"): vol.Any(str, None), + vol.Optional("entity_energy_from"): vol.Any(str, None), + vol.Optional("entity_energy_price"): vol.Any(str, None), + vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), + } +) def check_type_limits(value: list[SourceType]) -> list[SourceType]: @@ -197,6 +241,8 @@ ENERGY_SOURCE_SCHEMA = vol.All( { "grid": GRID_SOURCE_SCHEMA, "solar": SOLAR_SOURCE_SCHEMA, + "battery": BATTERY_SOURCE_SCHEMA, + "gas": GAS_SOURCE_SCHEMA, }, ) ] diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index 3a3cbeff4e7..5ddc6457a61 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/energy", "codeowners": ["@home-assistant/core"], "iot_class": "calculated", - "dependencies": ["websocket_api", "history"], + "dependencies": ["websocket_api", "history", "recorder"], "quality_scale": "internal" } diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index e974035cbd6..497c762add9 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -2,26 +2,24 @@ 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 ( - ATTR_LAST_RESET, DEVICE_CLASS_MONETARY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .const import DOMAIN from .data import EnergyManager, async_get_manager @@ -36,22 +34,19 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the energy sensors.""" - manager = await async_get_manager(hass) - process_now = partial(_process_manager_data, hass, manager, async_add_entities, {}) - manager.async_listen_updates(process_now) - - if manager.data: - await process_now() + sensor_manager = SensorManager(await async_get_manager(hass), async_add_entities) + await sensor_manager.async_start() T = TypeVar("T") @dataclass -class FlowAdapter: - """Adapter to allow flows to be used as sensors.""" +class SourceAdapter: + """Adapter to allow sources and their flows to be used as sensors.""" - flow_type: Literal["flow_from", "flow_to"] + source_type: Literal["grid", "gas"] + flow_type: Literal["flow_from", "flow_to", None] stat_energy_key: Literal["stat_energy_from", "stat_energy_to"] entity_energy_key: Literal["entity_energy_from", "entity_energy_to"] total_money_key: Literal["stat_cost", "stat_compensation"] @@ -59,8 +54,9 @@ class FlowAdapter: entity_id_suffix: str -FLOW_ADAPTERS: Final = ( - FlowAdapter( +SOURCE_ADAPTERS: Final = ( + SourceAdapter( + "grid", "flow_from", "stat_energy_from", "entity_energy_from", @@ -68,7 +64,8 @@ FLOW_ADAPTERS: Final = ( "Cost", "cost", ), - FlowAdapter( + SourceAdapter( + "grid", "flow_to", "stat_energy_to", "entity_energy_to", @@ -76,67 +73,112 @@ FLOW_ADAPTERS: Final = ( "Compensation", "compensation", ), + SourceAdapter( + "gas", + None, + "stat_energy_from", + "entity_energy_from", + "stat_cost", + "Cost", + "cost", + ), ) -async def _process_manager_data( - hass: HomeAssistant, - manager: EnergyManager, - async_add_entities: AddEntitiesCallback, - current_entities: dict[tuple[str, str], EnergyCostSensor], -) -> None: - """Process updated data.""" - to_add: list[SensorEntity] = [] - to_remove = dict(current_entities) +class SensorManager: + """Class to handle creation/removal of sensor data.""" - async def finish() -> None: - if to_add: - async_add_entities(to_add) + def __init__( + self, manager: EnergyManager, async_add_entities: AddEntitiesCallback + ) -> None: + """Initialize sensor manager.""" + self.manager = manager + self.async_add_entities = async_add_entities + self.current_entities: dict[tuple[str, str | None, str], EnergyCostSensor] = {} - for key, entity in to_remove.items(): - current_entities.pop(key) - await entity.async_remove() + async def async_start(self) -> None: + """Start.""" + self.manager.async_listen_updates(self._process_manager_data) + + if self.manager.data: + await self._process_manager_data() + + async def _process_manager_data(self) -> None: + """Process manager data.""" + to_add: list[SensorEntity] = [] + to_remove = dict(self.current_entities) + + async def finish() -> None: + if to_add: + self.async_add_entities(to_add) + + for key, entity in to_remove.items(): + self.current_entities.pop(key) + await entity.async_remove() + + if not self.manager.data: + await finish() + return + + for energy_source in self.manager.data["energy_sources"]: + for adapter in SOURCE_ADAPTERS: + if adapter.source_type != energy_source["type"]: + continue + + if adapter.flow_type is None: + self._process_sensor_data( + adapter, + # Opting out of the type complexity because can't get it to work + energy_source, # type: ignore + to_add, + to_remove, + ) + continue + + for flow in energy_source[adapter.flow_type]: # type: ignore + self._process_sensor_data( + adapter, + # Opting out of the type complexity because can't get it to work + flow, # type: ignore + to_add, + to_remove, + ) - if not manager.data: await finish() - return - for energy_source in manager.data["energy_sources"]: - if energy_source["type"] != "grid": - continue + @callback + def _process_sensor_data( + self, + adapter: SourceAdapter, + config: dict, + to_add: list[SensorEntity], + to_remove: dict[tuple[str, str | None, str], EnergyCostSensor], + ) -> None: + """Process sensor data.""" + # No need to create an entity if we already have a cost stat + if config.get(adapter.total_money_key) is not None: + return - for adapter in FLOW_ADAPTERS: - for flow in energy_source[adapter.flow_type]: - # Opting out of the type complexity because can't get it to work - untyped_flow = cast(dict, flow) + key = (adapter.source_type, adapter.flow_type, config[adapter.stat_energy_key]) - # No need to create an entity if we already have a cost stat - if untyped_flow.get(adapter.total_money_key) is not None: - continue + # Make sure the right data is there + # If the entity existed, we don't pop it from to_remove so it's removed + if config.get(adapter.entity_energy_key) is None or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None + ): + return - # This is unique among all flow_from's - key = (adapter.flow_type, untyped_flow[adapter.stat_energy_key]) + current_entity = to_remove.pop(key, None) + if current_entity: + current_entity.update_config(config) + return - # Make sure the right data is there - # If the entity existed, we don't pop it from to_remove so it's removed - if untyped_flow.get(adapter.entity_energy_key) is None or ( - untyped_flow.get("entity_energy_price") is None - and untyped_flow.get("number_energy_price") is None - ): - continue - - current_entity = to_remove.pop(key, None) - if current_entity: - current_entity.update_config(untyped_flow) - continue - - current_entities[key] = EnergyCostSensor( - adapter, - untyped_flow, - ) - to_add.append(current_entities[key]) - - await finish() + self.current_entities[key] = EnergyCostSensor( + adapter, + config, + ) + to_add.append(self.current_entities[key]) class EnergyCostSensor(SensorEntity): @@ -148,25 +190,26 @@ class EnergyCostSensor(SensorEntity): def __init__( self, - adapter: FlowAdapter, - flow: dict, + adapter: SourceAdapter, + config: dict, ) -> None: """Initialize the sensor.""" super().__init__() self._adapter = adapter - self.entity_id = f"{flow[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" + self.entity_id = ( + f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" + ) self._attr_device_class = DEVICE_CLASS_MONETARY - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._flow = flow - self._last_energy_sensor_state: State | None = None + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._config = config + self._last_energy_sensor_state: StateType | None = None self._cur_value = 0.0 - def _reset(self, energy_state: State) -> None: + def _reset(self, energy_state: StateType) -> None: """Reset the cost sensor.""" - self._attr_state = 0.0 + self._attr_native_value = 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() @@ -174,10 +217,10 @@ class EnergyCostSensor(SensorEntity): def _update_cost(self) -> None: """Update incurred costs.""" energy_state = self.hass.states.get( - cast(str, self._flow[self._adapter.entity_energy_key]) + cast(str, self._config[self._adapter.entity_energy_key]) ) - if energy_state is None or ATTR_LAST_RESET not in energy_state.attributes: + if energy_state is None: return try: @@ -186,8 +229,10 @@ class EnergyCostSensor(SensorEntity): return # Determine energy price - if self._flow["entity_energy_price"] is not None: - energy_price_state = self.hass.states.get(self._flow["entity_energy_price"]) + if self._config["entity_energy_price"] is not None: + energy_price_state = self.hass.states.get( + self._config["entity_energy_price"] + ) if energy_price_state is None: return @@ -197,51 +242,60 @@ class EnergyCostSensor(SensorEntity): except ValueError: return - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{ENERGY_WATT_HOUR}" + if ( + self._adapter.source_type == "grid" + and 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"]) + energy_price = cast(float, self._config["number_energy_price"]) if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. - self._reset(energy_state) + self._reset(energy_state.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: + if self._adapter.source_type == "grid": + if energy_unit == ENERGY_WATT_HOUR: + energy_price /= 1000 + elif energy_unit != ENERGY_KILO_WATT_HOUR: + energy_unit = None + + elif self._adapter.source_type == "gas": + if energy_unit != VOLUME_CUBIC_METERS: + energy_unit = None + + if energy_unit is None: _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] - ): + if energy < float(self._last_energy_sensor_state): # Energy meter was reset, reset cost sensor too - self._reset(energy_state) - else: - # Update with newly incurred cost - old_energy_value = float(self._last_energy_sensor_state.state) - self._cur_value += (energy - old_energy_value) * energy_price - self._attr_state = round(self._cur_value, 2) + self._reset(0) + # Update with newly incurred cost + old_energy_value = float(self._last_energy_sensor_state) + self._cur_value += (energy - old_energy_value) * energy_price + self._attr_native_value = round(self._cur_value, 2) - self._last_energy_sensor_state = energy_state + self._last_energy_sensor_state = energy_state.state async def async_added_to_hass(self) -> None: """Register callbacks.""" - energy_state = self.hass.states.get(self._flow[self._adapter.entity_energy_key]) + energy_state = self.hass.states.get( + self._config[self._adapter.entity_energy_key] + ) if energy_state: name = energy_state.name else: - name = split_entity_id(self._flow[self._adapter.entity_energy_key])[ + name = split_entity_id(self._config[self._adapter.entity_energy_key])[ 0 ].replace("_", " ") @@ -251,7 +305,7 @@ class EnergyCostSensor(SensorEntity): # Store stat ID in hass.data so frontend can look it up self.hass.data[DOMAIN]["cost_sensors"][ - self._flow[self._adapter.entity_energy_key] + self._config[self._adapter.entity_energy_key] ] = self.entity_id @callback @@ -263,7 +317,7 @@ class EnergyCostSensor(SensorEntity): self.async_on_remove( async_track_state_change_event( self.hass, - cast(str, self._flow[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.entity_energy_key]), async_state_changed_listener, ) ) @@ -271,16 +325,16 @@ class EnergyCostSensor(SensorEntity): async def async_will_remove_from_hass(self) -> None: """Handle removing from hass.""" self.hass.data[DOMAIN]["cost_sensors"].pop( - self._flow[self._adapter.entity_energy_key] + self._config[self._adapter.entity_energy_key] ) await super().async_will_remove_from_hass() @callback - def update_config(self, flow: dict) -> None: + def update_config(self, config: dict) -> None: """Update the config.""" - self._flow = flow + self._config = config @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the units of measurement.""" return self.hass.config.currency diff --git a/homeassistant/components/energy/translations/es.json b/homeassistant/components/energy/translations/es.json new file mode 100644 index 00000000000..64c2f5bffa1 --- /dev/null +++ b/homeassistant/components/energy/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Energ\u00eda" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/hu.json b/homeassistant/components/energy/translations/hu.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/no.json b/homeassistant/components/energy/translations/no.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hans.json b/homeassistant/components/energy/translations/zh-Hans.json new file mode 100644 index 00000000000..bae50fae66e --- /dev/null +++ b/homeassistant/components/energy/translations/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "title": "\u80fd\u6e90" +} \ No newline at end of file diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py new file mode 100644 index 00000000000..01709081d68 --- /dev/null +++ b/homeassistant/components/energy/validate.py @@ -0,0 +1,277 @@ +"""Validate the energy preferences provide valid data.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +from homeassistant.components import recorder, sensor +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback, valid_entity_id + +from . import data +from .const import DOMAIN + + +@dataclasses.dataclass +class ValidationIssue: + """Error or warning message.""" + + type: str + identifier: str + value: Any | None = None + + +@dataclasses.dataclass +class EnergyPreferencesValidation: + """Dictionary holding validation information.""" + + energy_sources: list[list[ValidationIssue]] = dataclasses.field( + default_factory=list + ) + device_consumption: list[list[ValidationIssue]] = dataclasses.field( + default_factory=list + ) + + def as_dict(self) -> dict: + """Return dictionary version.""" + return dataclasses.asdict(self) + + +@callback +def _async_validate_energy_stat( + hass: HomeAssistant, stat_value: str, result: list[ValidationIssue] +) -> None: + """Validate a statistic.""" + has_entity_source = valid_entity_id(stat_value) + + if not has_entity_source: + return + + if not recorder.is_entity_recorded(hass, stat_value): + result.append( + ValidationIssue( + "recorder_untracked", + stat_value, + ) + ) + return + + state = hass.states.get(stat_value) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + stat_value, + ) + ) + return + + if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + result.append(ValidationIssue("entity_unavailable", stat_value, state.state)) + return + + try: + current_value: float | None = float(state.state) + except ValueError: + result.append( + ValidationIssue("entity_state_non_numeric", stat_value, state.state) + ) + return + + if current_value is not None and current_value < 0: + result.append( + ValidationIssue("entity_negative_state", stat_value, current_value) + ) + + unit = state.attributes.get("unit_of_measurement") + + if unit not in (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR): + result.append( + ValidationIssue("entity_unexpected_unit_energy", stat_value, unit) + ) + + state_class = state.attributes.get("state_class") + + if state_class != sensor.STATE_CLASS_TOTAL_INCREASING: + result.append( + ValidationIssue( + "entity_unexpected_state_class_total_increasing", + stat_value, + state_class, + ) + ) + + +@callback +def _async_validate_price_entity( + hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the price entity is correct.""" + state = hass.states.get(entity_id) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + entity_id, + ) + ) + return + + try: + value: float | None = float(state.state) + except ValueError: + result.append( + ValidationIssue("entity_state_non_numeric", entity_id, state.state) + ) + return + + if value is not None and value < 0: + result.append(ValidationIssue("entity_negative_state", entity_id, value)) + + unit = state.attributes.get("unit_of_measurement") + + if unit is None or not unit.endswith( + (f"/{ENERGY_KILO_WATT_HOUR}", f"/{ENERGY_WATT_HOUR}") + ): + result.append(ValidationIssue("entity_unexpected_unit_price", entity_id, unit)) + + +@callback +def _async_validate_cost_stat( + hass: HomeAssistant, stat_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the cost stat is correct.""" + has_entity = valid_entity_id(stat_id) + + if not has_entity: + return + + if not recorder.is_entity_recorded(hass, stat_id): + result.append( + ValidationIssue( + "recorder_untracked", + stat_id, + ) + ) + + +@callback +def _async_validate_cost_entity( + hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the cost entity is correct.""" + if not recorder.is_entity_recorded(hass, entity_id): + result.append( + ValidationIssue( + "recorder_untracked", + entity_id, + ) + ) + + state = hass.states.get(entity_id) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + entity_id, + ) + ) + return + + state_class = state.attributes.get("state_class") + + if state_class != sensor.STATE_CLASS_TOTAL_INCREASING: + result.append( + ValidationIssue( + "entity_unexpected_state_class_total_increasing", entity_id, state_class + ) + ) + + +async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: + """Validate the energy configuration.""" + manager = await data.async_get_manager(hass) + + result = EnergyPreferencesValidation() + + if manager.data is None: + return result + + for source in manager.data["energy_sources"]: + source_result: list[ValidationIssue] = [] + result.energy_sources.append(source_result) + + if source["type"] == "grid": + for flow in source["flow_from"]: + _async_validate_energy_stat( + hass, flow["stat_energy_from"], source_result + ) + + if flow.get("stat_cost") is not None: + _async_validate_cost_stat(hass, flow["stat_cost"], source_result) + + elif flow.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, flow["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]], + source_result, + ) + + for flow in source["flow_to"]: + _async_validate_energy_stat(hass, flow["stat_energy_to"], source_result) + + if flow.get("stat_compensation") is not None: + _async_validate_cost_stat( + hass, flow["stat_compensation"], source_result + ) + + elif flow.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, flow["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]], + source_result, + ) + + elif source["type"] == "gas": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + + if source.get("stat_cost") is not None: + _async_validate_cost_stat(hass, source["stat_cost"], source_result) + + elif source.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, source["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]], + source_result, + ) + + elif source["type"] == "solar": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + + elif source["type"] == "battery": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + _async_validate_energy_stat(hass, source["stat_energy_to"], source_result) + + for device in manager.data["device_consumption"]: + device_result: list[ValidationIssue] = [] + result.device_consumption.append(device_result) + _async_validate_energy_stat(hass, device["stat_consumption"], device_result) + + return result diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index d1c8869a1c2..6d71a75b9b4 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -18,6 +18,7 @@ from .data import ( EnergyPreferencesUpdate, async_get_manager, ) +from .validate import async_validate EnergyWebSocketCommandHandler = Callable[ [HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"], @@ -35,6 +36,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_get_prefs) websocket_api.async_register_command(hass, ws_save_prefs) websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_validate) def _ws_with_manager( @@ -113,3 +115,18 @@ def ws_info( ) -> None: """Handle get info command.""" connection.send_result(msg["id"], hass.data[DOMAIN]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/validate", + } +) +@websocket_api.async_response +async def ws_validate( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Handle validate command.""" + connection.send_result(msg["id"], (await async_validate(hass)).as_dict()) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 1814efb9c87..ef7fe242092 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -130,12 +130,12 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/enocean/translations/hu.json b/homeassistant/components/enocean/translations/hu.json index 065747fb39d..bfb6cb0499d 100644 --- a/homeassistant/components/enocean/translations/hu.json +++ b/homeassistant/components/enocean/translations/hu.json @@ -1,7 +1,25 @@ { "config": { "abort": { + "invalid_dongle_path": "\u00c9rv\u00e9nytelen dongle \u00fatvonal", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_dongle_path": "Nem tal\u00e1lhat\u00f3 \u00e9rv\u00e9nyes dongle ehhez az \u00fatvonalhoz" + }, + "step": { + "detect": { + "data": { + "path": "USB dongle el\u00e9r\u00e9si \u00fatja" + }, + "title": "V\u00e1lassza ki az ENOcean-dongle el\u00e9r\u00e9si \u00fatvonal\u00e1t." + }, + "manual": { + "data": { + "path": "USB dongle el\u00e9r\u00e9si \u00fatja" + }, + "title": "Adja meg az ENOcean dongle el\u00e9r\u00e9si \u00fatvonal\u00e1t" + } } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 9f87a821787..ff42ef23746 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -3,10 +3,10 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntityDescription, ) from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_WATT_HOUR, POWER_WATT -from homeassistant.util import dt DOMAIN = "enphase_envoy" @@ -20,63 +20,61 @@ SENSORS = ( SensorEntityDescription( key="production", name="Current Power Production", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="daily_production", name="Today's Energy Production", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, ), SensorEntityDescription( key="seven_days_production", name="Last Seven Days Energy Production", - unit_of_measurement=ENERGY_WATT_HOUR, + native_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, + native_unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), ), SensorEntityDescription( key="consumption", name="Current Power Consumption", - unit_of_measurement=POWER_WATT, + native_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, + native_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, + native_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, + native_unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), ), SensorEntityDescription( key="inverters", name="Inverter", - unit_of_measurement=POWER_WATT, + native_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 29d273401f4..9bf4073847e 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, } @@ -130,7 +132,7 @@ class Envoy(CoordinatorEntity, SensorEntity): return f"{self._device_serial_number}_{self.entity_description.key}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.entity_description.key != "inverters": value = self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hans.json b/homeassistant/components/enphase_envoy/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 0852f95bd99..cad8a49884f 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -168,7 +168,7 @@ class EnturPublicTransportSensor(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self._state @@ -180,7 +180,7 @@ class EnturPublicTransportSensor(SensorEntity): return self._attributes @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 019dcb1aee5..ecd0c562d16 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,4 +1,6 @@ """Support for the Environment Canada radar imagery.""" +from __future__ import annotations + import datetime from env_canada import ECRadar @@ -68,7 +70,9 @@ class ECCamera(Camera): self.image = None self.timestamp = None - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" self.update() return self.image diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2c16eca9ea1..3690703d8d2 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -91,7 +91,7 @@ class ECSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -101,7 +101,7 @@ class ECSensor(SensorEntity): return self._attr @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 9bca552326a..a41b1678faa 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -88,7 +88,7 @@ class EnvirophatSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -103,7 +103,7 @@ class EnvirophatSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 6fd7f32c6fe..88aa7fa988c 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -61,7 +61,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the overall state.""" return self._info["status"]["alpha"] diff --git a/homeassistant/components/epson/translations/zh-Hans.json b/homeassistant/components/epson/translations/zh-Hans.json new file mode 100644 index 00000000000..3cb7f97ceb9 --- /dev/null +++ b/homeassistant/components/epson/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "powered_off": "\u6295\u5f71\u4eea\u662f\u5426\u5df2\u7ecf\u6253\u5f00\uff1f\u60a8\u9700\u8981\u6253\u5f00\u6295\u5f71\u4eea\u4ee5\u8fdb\u884c\u521d\u59cb\u914d\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 2f483b9fcbf..285f2fc83e7 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -20,37 +20,37 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="black", name="Ink level Black", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="photoblack", name="Ink level Photoblack", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="magenta", name="Ink level Magenta", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="cyan", name="Ink level Cyan", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="yellow", name="Ink level Yellow", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="clean", name="Cleaning level", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ) MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] @@ -92,7 +92,7 @@ class EpsonPrinterCartridge(SensorEntity): self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._api.getSensorValue(self.entity_description.key) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 938d78362f7..47010324290 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -50,7 +50,9 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): async with self._image_cond: self._image_cond.notify_all() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return single camera image bytes.""" if not self.available: return None diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index b89a75ab76a..73339769121 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: @@ -157,7 +157,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): 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 + supported_modes = self._native_supported_color_modes + if LightColorMode.COLOR_TEMPERATURE in supported_modes: + data["color_mode"] = LightColorMode.COLOR_TEMPERATURE + elif LightColorMode.COLD_WARM_WHITE in supported_modes: + data["color_mode"] = LightColorMode.COLD_WARM_WHITE if (effect := kwargs.get(ATTR_EFFECT)) is not None: data["effect"] = effect @@ -230,7 +234,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) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 6a2b51498f0..3e8fbc19a4d 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -161,7 +161,7 @@ class EsphomeSensor( return self._static_info.force_update @esphome_state_property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None @@ -172,7 +172,7 @@ class EsphomeSensor( return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if not self._static_info.unit_of_measurement: return None @@ -202,7 +202,7 @@ class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEn return self._static_info.icon @esphome_state_property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if self._state.missing_state: return None diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index f0dc70d7be4..42a4c1c399b 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -104,12 +104,12 @@ class EssentMeter(SensorEntity): return f"Essent {self._type} ({self._tariff})" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self._unit.lower() == "kwh": return ENERGY_KILO_WATT_HOUR diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 1b10cc39fe1..b1ec3cddb0c 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -59,12 +59,12 @@ class EtherscanSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 76fbaee3757..44a90e2928f 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,13 +1,12 @@ """Support ezviz camera devices.""" from __future__ import annotations -import asyncio import logging -from haffmpeg.tools import IMAGE_JPEG, ImageFrame from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError import voluptuous as vol +from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import ( @@ -325,14 +324,15 @@ class EzvizCamera(CoordinatorEntity, Camera): """Return the name of this camera.""" return self._serial - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a frame from the camera stream.""" - ffmpeg = ImageFrame(self._ffmpeg.binary) - - image = await asyncio.shield( - ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG) + if self._rtsp_stream is None: + return None + return await ffmpeg.async_get_image( + self.hass, self._rtsp_stream, width=width, height=height ) - return image @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 4e81ef6a6a7..512491a2548 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -5,9 +5,10 @@ import logging from pyezviz.constants import SensorType +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -39,7 +40,7 @@ async def async_setup_entry( async_add_entities(sensors) -class EzvizSensor(CoordinatorEntity, Entity): +class EzvizSensor(CoordinatorEntity, SensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator @@ -66,7 +67,7 @@ class EzvizSensor(CoordinatorEntity, Entity): return self._name @property - def state(self) -> int | str: + def native_value(self) -> int | str: """Return the state of the sensor.""" return self.coordinator.data[self._idx][self._name] diff --git a/homeassistant/components/ezviz/translations/zh-Hans.json b/homeassistant/components/ezviz/translations/zh-Hans.json new file mode 100644 index 00000000000..3d8daedec73 --- /dev/null +++ b/homeassistant/components/ezviz/translations/zh-Hans.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "ezviz_cloud_account_missing": "\u8424\u77f3\u4e91\u8d26\u53f7\u4e22\u5931\u3002\u8bf7\u91cd\u65b0\u914d\u7f6e\u8424\u77f3\u4e91\u8d26\u53f7\u3002", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548", + "invalid_host": "\u65e0\u6548\u7684\u4e3b\u673a\u5730\u5740\u6216 IP \u5730\u5740" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8f93\u5165\u5e26\u6709 RTSP \u51ed\u8bc1\u7684\u8424\u77f3\u6444\u50cf\u5934{serial} IP {ip_address} ", + "title": "\u5df2\u53d1\u73b0\u7684\u8424\u77f3\u6444\u50cf\u5934" + }, + "user": { + "data": { + "password": "\u5bc6\u7801", + "url": "URL", + "username": "\u7528\u6237\u540d" + }, + "title": "\u8fde\u63a5\u5230\u8424\u77f3\u4e91" + }, + "user_custom_url": { + "data": { + "password": "\u5bc6\u7801", + "url": "URL", + "username": "\u7528\u6237\u540d" + }, + "description": "\u624b\u52a8\u6307\u5b9a\u4f60\u7684\u533a\u57df\u7f51\u5740", + "title": "\u8fde\u63a5\u5230\u81ea\u5b9a\u4e49\u8424\u77f3\u4e91\u5730\u5740" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "FFmpeg \u53c2\u6570\u4f20\u9012\u81f3\u6444\u50cf\u673a", + "timeout": "\u8bf7\u6c42\u8d85\u65f6\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 908ab5d77c0..5a7e1052b67 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -70,7 +70,7 @@ class BanSensor(SensorEntity): return self.ban_dict @property - def state(self): + def native_value(self): """Return the most recently banned IP Address.""" return self.last_ban diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index ea654074a5a..65b7a63e419 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,4 +1,6 @@ """Family Hub camera for Samsung Refrigerators.""" +from __future__ import annotations + from pyfamilyhublocal import FamilyHubCam import voluptuous as vol @@ -38,7 +40,9 @@ class FamilyHubCamera(Camera): self._name = name self.family_hub_cam = family_hub_cam - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response.""" return await self.family_hub_cam.async_get_cam_image() diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 1d0caa3231b..a05505e8112 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( ordered_list_item_to_percentage, @@ -124,7 +125,7 @@ def is_on(hass, entity_id: str) -> bool: return state.state == STATE_ON -async def async_setup(hass, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Expose fan control via statemachine and services.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 14f63a99e5d..fa1f18815f1 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -30,10 +30,10 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): """Implementation of a FAst.com sensor.""" _attr_name = "Fast.com Download" - _attr_unit_of_measurement = DATA_RATE_MEGABITS_PER_SECOND + _attr_native_unit_of_measurement = DATA_RATE_MEGABITS_PER_SECOND _attr_icon = ICON _attr_should_poll = False - _attr_state = None + _attr_native_value = None def __init__(self, speedtest_data: dict[str, Any]) -> None: """Initialize the sensor.""" @@ -52,14 +52,14 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): state = await self.async_get_last_state() if not state: return - self._attr_state = state.state + self._attr_native_value = state.state def update(self) -> None: """Get the latest data and update the states.""" data = self._speedtest_data.data # type: ignore[attr-defined] if data is None: return - self._attr_state = data["download"] + self._attr_native_value = data["download"] @callback def _schedule_immediate_update(self) -> None: diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 52e034c6265..74c826f47d6 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from homeassistant.loader import bind_hass DOMAIN = "ffmpeg" @@ -89,15 +90,26 @@ async def async_setup(hass, config): return True +@bind_hass async def async_get_image( hass: HomeAssistant, input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: str | None = None, + width: int | None = None, + height: int | None = None, ) -> bytes | None: """Get an image from a frame of an RTSP stream.""" manager = hass.data[DATA_FFMPEG] ffmpeg = ImageFrame(manager.binary) + + if width and height and (extra_cmd is None or "-s" not in extra_cmd): + size_cmd = f"-s {width}x{height}" + if extra_cmd is None: + extra_cmd = size_cmd + else: + extra_cmd += " " + size_cmd + image = await asyncio.shield( ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd) ) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 4cd8b0d1453..323eae7c129 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -1,4 +1,5 @@ """Support for Cameras with FFmpeg as decoder.""" +from __future__ import annotations from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG @@ -49,7 +50,9 @@ class FFmpegCamera(Camera): """Return the stream source.""" return self._input.split(" ")[-1] - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return await async_get_image( self.hass, diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 3161e173b2a..a4b4e744af7 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -85,12 +85,12 @@ class FibaroSensor(FibaroDevice, SensorEntity): self._unit = self.fibaro_device.properties.unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 55ec455d8f1..0723e097967 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -109,12 +109,12 @@ class FidoSensor(SensorEntity): return f"{self.client_name} {self._number} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 5d8a9475235..73b262c9090 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -62,7 +62,7 @@ class FileSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -72,7 +72,7 @@ class FileSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 856b29364ae..dc44d3d8255 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -63,7 +63,7 @@ class Filesize(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the size of the file in MB.""" decimals = 2 state_mb = round(self._size / 1e6, decimals) @@ -84,6 +84,6 @@ class Filesize(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 97412823b30..f9705887549 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -332,7 +332,7 @@ class SensorFilter(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -342,7 +342,7 @@ class SensorFilter(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement @@ -405,7 +405,7 @@ class Filter: :param entity: used for debugging only """ if isinstance(window_size, int): - self.states = deque(maxlen=window_size) + self.states: deque = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS else: self.states = deque(maxlen=0) @@ -476,7 +476,7 @@ class RangeFilter(Filter, SensorEntity): super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound - self._stats_internal = Counter() + self._stats_internal: Counter = Counter() def _filter_state(self, new_state): """Implement the range filter.""" @@ -522,7 +522,7 @@ class OutlierFilter(Filter, SensorEntity): """ super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius - self._stats_internal = Counter() + self._stats_internal: Counter = Counter() self._store_raw = True def _filter_state(self, new_state): diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 9159e0df49a..d584bbed4bb 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -179,8 +179,8 @@ class FinTsAccount(SensorEntity): """Get the current balance and currency for the account.""" bank = self._client.client balance = bank.get_balance(self._account) - self._attr_state = balance.amount.amount - self._attr_unit_of_measurement = balance.amount.currency + self._attr_native_value = balance.amount.amount + self._attr_native_unit_of_measurement = balance.amount.currency _LOGGER.debug("updated balance of account %s", self.name) @@ -198,13 +198,13 @@ class FinTsHoldingsAccount(SensorEntity): self._account = account self._holdings: list[Any] = [] self._attr_icon = ICON - self._attr_unit_of_measurement = "EUR" + self._attr_native_unit_of_measurement = "EUR" def update(self) -> None: """Get the current holdings for the account.""" bank = self._client.client self._holdings = bank.get_holdings(self._account) - self._attr_state = sum(h.total_value for h in self._holdings) + self._attr_native_value = sum(h.total_value for h in self._holdings) @property def extra_state_attributes(self) -> dict: diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 58b3239331c..ec446621212 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -49,7 +49,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): return "mdi:fire-truck" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index 24b6420e8a5..d98866f900b 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType from .board import FirmataBoard from .const import ( @@ -122,7 +123,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Firmata domain.""" # Delete specific entries that no longer exist in the config if hass.config_entries.async_entries(DOMAIN): diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py index fedac6f76d9..b46e96f3c25 100644 --- a/homeassistant/components/firmata/sensor.py +++ b/homeassistant/components/firmata/sensor.py @@ -54,6 +54,6 @@ class FirmataSensor(FirmataPinEntity, SensorEntity): await self._api.stop_pin() @property - def state(self) -> int: + def native_value(self) -> int: """Return sensor state.""" return self._api.state diff --git a/homeassistant/components/firmata/translations/hu.json b/homeassistant/components/firmata/translations/hu.json index 563ede56155..8224d177a9f 100644 --- a/homeassistant/components/firmata/translations/hu.json +++ b/homeassistant/components/firmata/translations/hu.json @@ -2,6 +2,10 @@ "config": { "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "one": "\u00dcres", + "other": "\u00dcres" } } } \ No newline at end of file diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 9f99b3d0bb0..0bd4ed36199 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -374,12 +374,12 @@ class FitbitSensor(SensorEntity): return self._name @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 9214dd6907e..3108f7d3272 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -64,12 +64,12 @@ class ExchangeRateSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._target @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index cf4662b9866..ce3e5a68e1e 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from pyflexit.pyflexit import pyflexit import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -12,7 +11,15 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN +from homeassistant.components.modbus import get_hub +from homeassistant.components.modbus.const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_REGISTER, + CONF_HUB, + DEFAULT_HUB, +) +from homeassistant.components.modbus.modbus import ModbusHub from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, @@ -20,7 +27,9 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -35,18 +44,25 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities, + discovery_info: DiscoveryInfoType = None, +): """Set up the Flexit Platform.""" modbus_slave = config.get(CONF_SLAVE) name = config.get(CONF_NAME) - hub = hass.data[MODBUS_DOMAIN][config.get(CONF_HUB)] - add_entities([Flexit(hub, modbus_slave, name)], True) + hub = get_hub(hass, config[CONF_HUB]) + async_add_entities([Flexit(hub, modbus_slave, name)], True) class Flexit(ClimateEntity): """Representation of a Flexit AC unit.""" - def __init__(self, hub, modbus_slave, name): + def __init__( + self, hub: ModbusHub, modbus_slave: int | None, name: str | None + ) -> None: """Initialize the unit.""" self._hub = hub self._name = name @@ -64,34 +80,65 @@ class Flexit(ClimateEntity): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit(hub, modbus_slave) + self._outdoor_air_temp = None @property def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS - def update(self): + async def async_update(self): """Update unit attributes.""" - if not self.unit.update(): - _LOGGER.warning("Modbus read failed") + self._target_temperature = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_HOLDING, 8 + ) + self._current_temperature = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_INPUT, 9 + ) + res = await self._async_read_int16_from_register(CALL_TYPE_REGISTER_HOLDING, 17) + if res < len(self._fan_modes): + self._current_fan_mode = res + self._filter_hours = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 8 + ) + # # Mechanical heat recovery, 0-100% + self._heat_recovery = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 14 + ) + # # Heater active 0-100% + self._heating = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 15 + ) + # # Cooling active 0-100% + self._cooling = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 13 + ) + # # Filter alarm 0/1 + self._filter_alarm = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 27 + ) + # # Heater enabled or not. Does not mean it's necessarily heating + self._heater_enabled = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 28 + ) + self._outdoor_air_temp = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_INPUT, 11 + ) - self._target_temperature = self.unit.get_target_temp - self._current_temperature = self.unit.get_temp - self._current_fan_mode = self._fan_modes[self.unit.get_fan_speed] - self._filter_hours = self.unit.get_filter_hours - # Mechanical heat recovery, 0-100% - self._heat_recovery = self.unit.get_heat_recovery - # Heater active 0-100% - self._heating = self.unit.get_heating - # Cooling active 0-100% - self._cooling = self.unit.get_cooling - # Filter alarm 0/1 - self._filter_alarm = self.unit.get_filter_alarm - # Heater enabled or not. Does not mean it's necessarily heating - self._heater_enabled = self.unit.get_heater_enabled - # Current operation mode - self._current_operation = self.unit.get_operation + actual_air_speed = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 48 + ) + + if self._heating: + self._current_operation = "Heating" + elif self._cooling: + self._current_operation = "Cooling" + elif self._heat_recovery: + self._current_operation = "Recovering" + elif actual_air_speed: + self._current_operation = "Fan Only" + else: + self._current_operation = "Off" @property def extra_state_attributes(self): @@ -103,6 +150,7 @@ class Flexit(ClimateEntity): "heating": self._heating, "heater_enabled": self._heater_enabled, "cooling": self._cooling, + "outdoor_air_temp": self._outdoor_air_temp, } @property @@ -153,12 +201,53 @@ class Flexit(ClimateEntity): """Return the list of available fan modes.""" return self._fan_modes - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: - self._target_temperature = kwargs.get(ATTR_TEMPERATURE) - self.unit.set_temp(self._target_temperature) + target_temperature = kwargs.get(ATTR_TEMPERATURE) + else: + _LOGGER.error("Received invalid temperature") + return - def set_fan_mode(self, fan_mode): + if await self._async_write_int16_to_register(8, target_temperature * 10): + self._target_temperature = target_temperature + else: + _LOGGER.error("Modbus error setting target temperature to Flexit") + + async def async_set_fan_mode(self, fan_mode): """Set new fan mode.""" - self.unit.set_fan_speed(self._fan_modes.index(fan_mode)) + if await self._async_write_int16_to_register( + 17, self.fan_modes.index(fan_mode) + ): + self._current_fan_mode = self.fan_modes.index(fan_mode) + else: + _LOGGER.error("Modbus error setting fan mode to Flexit") + + # Based on _async_read_register in ModbusThermostat class + async def _async_read_int16_from_register(self, register_type, register) -> int: + """Read register using the Modbus hub slave.""" + result = await self._hub.async_pymodbus_call( + self._slave, register, 1, register_type + ) + if result is None: + _LOGGER.error("Error reading value from Flexit modbus adapter") + return -1 + + return int(result.registers[0]) + + async def _async_read_temp_from_register(self, register_type, register) -> float: + result = float( + await self._async_read_int16_from_register(register_type, register) + ) + if result == -1: + return -1 + return result / 10.0 + + async def _async_write_int16_to_register(self, register, value) -> bool: + value = int(value) + result = await self._hub.async_pymodbus_call( + self._slave, register, value, CALL_TYPE_WRITE_REGISTER + ) + if result == -1: + return False + return True diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 96ed5b55904..d9f84d5ab81 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -2,7 +2,6 @@ "domain": "flexit", "name": "Flexit", "documentation": "https://www.home-assistant.io/integrations/flexit", - "requirements": ["pyflexit==0.3"], "dependencies": ["modbus"], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index ab628e205c7..938507e4b0c 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry( class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" - _attr_unit_of_measurement = UNIT_NAME + _attr_native_unit_of_measurement = UNIT_NAME def __init__(self, api: FlickAPI) -> None: """Entity object for Flick Electric sensor.""" @@ -53,7 +53,7 @@ class FlickPricingSensor(SensorEntity): return FRIENDLY_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._price.price diff --git a/homeassistant/components/flick_electric/translations/hu.json b/homeassistant/components/flick_electric/translations/hu.json index f7ed726e433..90ea92089e1 100644 --- a/homeassistant/components/flick_electric/translations/hu.json +++ b/homeassistant/components/flick_electric/translations/hu.json @@ -11,6 +11,8 @@ "step": { "user": { "data": { + "client_id": "Kliens ID (opcion\u00e1lis)", + "client_secret": "Kliens jelsz\u00f3 (nem k\u00f6telez\u0151)", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 05bbd0d5449..66ea93484f7 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -54,8 +54,6 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): password = entry.data[CONF_PASSWORD] self.flipr_id = entry.data[CONF_FLIPR_ID] - _LOGGER.debug("Config entry values : %s, %s", username, self.flipr_id) - # Establishes the connection. self.client = FliprAPIRestClient(username, password) self.entry = entry diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index b503281fed4..b1e4f31d044 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" _LOGGER.exception(exception) - if not errors and len(flipr_ids) == 0: + if not errors and not flipr_ids: # No flipr_id found. Tell the user with an error message. errors["base"] = "no_flipr_id_found" @@ -85,9 +85,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _authenticate_and_search_flipr(self) -> list[str]: """Validate the username and password provided and searches for a flipr id.""" - client = await self.hass.async_add_executor_job( - FliprAPIRestClient, self._username, self._password - ) + # Instantiates the flipr API that does not require async since it is has no network access. + client = FliprAPIRestClient(self._username, self._password) flipr_ids = await self.hass.async_add_executor_job(client.search_flipr_ids) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 427a668a72b..f9fd4e9633e 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,20 +1,21 @@ """Sensor platform for the Flipr's pool_sensor.""" from datetime import datetime +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ELECTRIC_POTENTIAL_MILLIVOLT, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import FliprEntity from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN SENSORS = { "chlorine": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Chlorine", "device_class": None, @@ -33,7 +34,7 @@ SENSORS = { "device_class": DEVICE_CLASS_TIMESTAMP, }, "red_ox": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Red OX", "device_class": None, @@ -53,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors_list, True) -class FliprSensor(FliprEntity, Entity): +class FliprSensor(FliprEntity, SensorEntity): """Sensor representing FliprSensor data.""" @property @@ -62,7 +63,7 @@ class FliprSensor(FliprEntity, Entity): return f"Flipr {self.flipr_id} {SENSORS[self.info_type]['name']}" @property - def state(self): + def native_value(self): """State of the sensor.""" state = self.coordinator.data[self.info_type] if isinstance(state, datetime): @@ -80,7 +81,7 @@ class FliprSensor(FliprEntity, Entity): return SENSORS[self.info_type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return SENSORS[self.info_type]["unit"] diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json new file mode 100644 index 00000000000..766f83856ec --- /dev/null +++ b/homeassistant/components/flipr/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "no_flipr_id_found": "Por ahora no hay ning\u00fan ID de Flipr asociado a tu cuenta. Deber\u00edas verificar que est\u00e1 funcionando con la aplicaci\u00f3n m\u00f3vil de Flipr primero.", + "unknown": "Error desconocido" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "ID de Flipr" + }, + "description": "Elija su ID de Flipr en la lista", + "title": "Elige tu Flipr" + }, + "user": { + "data": { + "email": "Correo-e", + "password": "Clave" + }, + "description": "Con\u00e9ctese usando su cuenta Flipr.", + "title": "Conectarse a Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/hu.json b/homeassistant/components/flipr/translations/hu.json new file mode 100644 index 00000000000..4daf0446abc --- /dev/null +++ b/homeassistant/components/flipr/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_flipr_id_found": "A fi\u00f3kj\u00e1hoz jelenleg nem tartozik Flipr-azonos\u00edt\u00f3. El\u0151sz\u00f6r ellen\u0151riznie kell, hogy m\u0171k\u00f6dik-e a Flipr mobilalkalmaz\u00e1s\u00e1val.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr azonos\u00edt\u00f3" + }, + "description": "V\u00e1lassza ki a Flipr azonos\u00edt\u00f3j\u00e1t a list\u00e1b\u00f3l", + "title": "V\u00e1lassza ki a Flipr-t" + }, + "user": { + "data": { + "email": "Email", + "password": "Jelsz\u00f3" + }, + "description": "Csatlakozzon a Flipr-fi\u00f3kj\u00e1val.", + "title": "Csatlakoz\u00e1s a Flipr-hez" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/no.json b/homeassistant/components/flipr/translations/no.json new file mode 100644 index 00000000000..550b0bae058 --- /dev/null +++ b/homeassistant/components/flipr/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "no_flipr_id_found": "Ingen flipr -ID er knyttet til kontoen din forel\u00f8pig. Du b\u00f8r bekrefte at den fungerer med Flipr -mobilappen f\u00f8rst.", + "unknown": "Uventet feil" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Velg din Flipr -ID i listen", + "title": "Velg din Flipr" + }, + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "description": "Koble til ved hjelp av Flipr-kontoen din.", + "title": "Koble til Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/zh-Hans.json b/homeassistant/components/flipr/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/flipr/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 0504d451e14..b64ed9ee3e4 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -61,7 +61,7 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" _attr_icon = WATER_ICON - _attr_unit_of_measurement = VOLUME_GALLONS + _attr_native_unit_of_measurement = VOLUME_GALLONS def __init__(self, device): """Initialize the daily water usage sensor.""" @@ -69,7 +69,7 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current daily usage.""" if self._device.consumption_today is None: return None @@ -85,7 +85,7 @@ class FloSystemModeSensor(FloEntity, SensorEntity): self._state: str = None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the current system mode.""" if not self._device.current_system_mode: return None @@ -96,7 +96,7 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): """Monitors the current water flow rate.""" _attr_icon = GAUGE_ICON - _attr_unit_of_measurement = "gpm" + _attr_native_unit_of_measurement = "gpm" def __init__(self, device): """Initialize the flow rate sensor.""" @@ -104,7 +104,7 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current flow rate.""" if self._device.current_flow_rate is None: return None @@ -115,7 +115,7 @@ class FloTemperatureSensor(FloEntity, SensorEntity): """Monitors the temperature.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_FAHRENHEIT + _attr_native_unit_of_measurement = TEMP_FAHRENHEIT def __init__(self, name, device): """Initialize the temperature sensor.""" @@ -123,7 +123,7 @@ class FloTemperatureSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current temperature.""" if self._device.temperature is None: return None @@ -134,7 +134,7 @@ class FloHumiditySensor(FloEntity, SensorEntity): """Monitors the humidity.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): """Initialize the humidity sensor.""" @@ -142,7 +142,7 @@ class FloHumiditySensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current humidity.""" if self._device.humidity is None: return None @@ -153,7 +153,7 @@ class FloPressureSensor(FloEntity, SensorEntity): """Monitors the water pressure.""" _attr_device_class = DEVICE_CLASS_PRESSURE - _attr_unit_of_measurement = PRESSURE_PSI + _attr_native_unit_of_measurement = PRESSURE_PSI def __init__(self, device): """Initialize the pressure sensor.""" @@ -161,7 +161,7 @@ class FloPressureSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current water pressure.""" if self._device.current_psi is None: return None @@ -172,7 +172,7 @@ class FloBatterySensor(FloEntity, SensorEntity): """Monitors the battery level for battery-powered leak detectors.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): """Initialize the battery sensor.""" @@ -180,6 +180,6 @@ class FloBatterySensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current battery level.""" return self._device.battery_level diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index d890443d238..ee67a863be6 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -136,7 +136,7 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" sensor_key = self._flume_query_sensor[0] if sensor_key not in self._flume_device.values: @@ -145,7 +145,7 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return _format_state_value(self._flume_device.values[sensor_key]) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" # This is in gallons per SCAN_INTERVAL return self._flume_query_sensor[1]["unit_of_measurement"] diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index e607ac4255e..e1780be5654 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -19,9 +19,13 @@ }, "user": { "data": { + "client_id": "\u00dcgyf\u00e9lazonos\u00edt\u00f3", + "client_secret": "\u00dcgyf\u00e9l jelszva", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A Flume Personal API el\u00e9r\u00e9s\u00e9hez \u201e\u00dcgyf\u00e9l-azonos\u00edt\u00f3t\u201d \u00e9s \u201e\u00dcgyf\u00e9ltitkot\u201d kell k\u00e9rnie a https://portal.flumetech.com/settings#token c\u00edmen.", + "title": "Csatlakozzon a Flume-fi\u00f3kj\u00e1hoz" } } } diff --git a/homeassistant/components/flume/translations/zh-Hans.json b/homeassistant/components/flume/translations/zh-Hans.json index a5f4ff11f09..db06c3cf23a 100644 --- a/homeassistant/components/flume/translations/zh-Hans.json +++ b/homeassistant/components/flume/translations/zh-Hans.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "{username} \u7684\u5bc6\u7801\u5df2\u5931\u6548\u3002" + }, "user": { "data": { "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 88fb0147296..e28419c5d06 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -116,7 +116,7 @@ class FluNearYouSensor(CoordinatorEntity, SensorEntity): f"{entry.data[CONF_LATITUDE]}," f"{entry.data[CONF_LONGITUDE]}_{sensor_type}" ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._entry = entry self._sensor_type = sensor_type @@ -149,7 +149,7 @@ class CdcSensor(FluNearYouSensor): ATTR_STATE: self.coordinator.data["name"], } ) - self._attr_state = self.coordinator.data[self._sensor_type] + self._attr_native_value = self.coordinator.data[self._sensor_type] class UserSensor(FluNearYouSensor): @@ -181,7 +181,7 @@ class UserSensor(FluNearYouSensor): ] = self.coordinator.data["state"]["last_week_data"][states_key] if self._sensor_type == SENSOR_TYPE_USER_TOTAL: - self._attr_state = sum( + self._attr_native_value = sum( v for k, v in self.coordinator.data["local"].items() if k @@ -194,4 +194,4 @@ class UserSensor(FluNearYouSensor): ) ) else: - self._attr_state = self.coordinator.data["local"][self._sensor_type] + self._attr_native_value = self.coordinator.data["local"][self._sensor_type] diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json index 4f8cca2a939..a67bc91a2a1 100644 --- a/homeassistant/components/flunearyou/translations/hu.json +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -11,7 +11,9 @@ "data": { "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" - } + }, + "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra.", + "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 707f22f98ba..c7257d40237 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -79,7 +79,7 @@ class Folder(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" decimals = 2 size_mb = round(self._size / 1e6, decimals) @@ -102,6 +102,6 @@ class Folder(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index d635f231818..dd9f086d0d9 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -126,7 +126,7 @@ class FoobotSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def state(self): + def native_value(self): """Return the state of the device.""" try: data = self.foobot_data.data[self.type] @@ -140,7 +140,7 @@ class FoobotSensor(SensorEntity): return f"{self._uuid}_{self.type}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return self._unit_of_measurement diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 7ae6fe01d42..ea76ed7da2a 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -30,14 +30,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Energy Production - Today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", name="Estimated Energy Production - Tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", @@ -55,7 +55,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_POWER, state=lambda estimate: estimate.power_production_now, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_hour", @@ -65,7 +65,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next Hour", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_12hours", @@ -75,7 +75,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next 12 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_24hours", @@ -85,20 +85,20 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next 24 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", name="Estimated Energy Production - This Hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, name="Estimated Energy Production - Next Hour", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 5d3f440f4b6..29ba14ac463 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -60,7 +60,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" if self.entity_description.state is None: state: StateType | datetime = getattr( diff --git a/homeassistant/components/forecast_solar/translations/cs.json b/homeassistant/components/forecast_solar/translations/cs.json new file mode 100644 index 00000000000..0b970643bbe --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 2189cb91f77..8a1b51a5084 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -1,8 +1,21 @@ { + "config": { + "step": { + "user": { + "data": { + "azimuth": "Acimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", + "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", + "modules power": "Potencia total en vatios pico de sus m\u00f3dulos solares" + }, + "description": "Rellene los datos de sus paneles solares. Consulte la documentaci\u00f3n si alg\u00fan campo no est\u00e1 claro." + } + } + }, "options": { "step": { "init": { "data": { + "api_key": "Clave API de Forecast.Solar (opcional)", "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json index 5ee0691ecda..1504727c1ae 100644 --- a/homeassistant/components/forecast_solar/translations/no.json +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -24,7 +24,7 @@ "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)", "modules power": "Total Watt-toppeffekt i solcellemodulene dine" }, - "description": "Disse verdiene tillater justering av Solar.Forecast-resultatet. Se dokumentasjonen er et felt som er uklart." + "description": "Disse verdiene tillater justering av Solar.Forecast -resultatet. Se dokumentasjonen hvis et felt er uklart." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 3400984dcd6..bbf8cb560ff 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -1,18 +1,41 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." }, "error": { "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", - "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", + "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot.", + "wrong_password": "Helytelen jelsz\u00f3.", + "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja> = 27.0." }, "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Hoszt" - } + "host": "Hoszt", + "name": "Megjelen\u00edt\u00e9si n\u00e9v", + "password": "API jelsz\u00f3 (hagyja \u00fcresen, ha nincs jelsz\u00f3)", + "port": "API port" + }, + "title": "\u00c1ll\u00edtsa be a forked-daapd eszk\u00f6zt" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port librespot-java cs\u0151 vez\u00e9rl\u00e9s (ha van)", + "max_playlists": "Forr\u00e1sk\u00e9nt haszn\u00e1lt lej\u00e1tsz\u00e1si list\u00e1k maxim\u00e1lis sz\u00e1ma", + "tts_pause_time": "M\u00e1sodpercek a TTS el\u0151tti \u00e9s ut\u00e1ni sz\u00fcnethez", + "tts_volume": "TTS hanger\u0151 (lebeg\u0151 a [0,1] tartom\u00e1nyban)" + }, + "description": "A forked-daapd integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sai.", + "title": "A forked-daapd be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/forked_daapd/translations/zh-Hans.json b/homeassistant/components/forked_daapd/translations/zh-Hans.json new file mode 100644 index 00000000000..9b2bd981397 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/zh-Hans.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "not_forked_daapd": "\u6b64\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a forked-daapd \u670d\u52a1\u5668\u3002" + }, + "error": { + "forbidden": "\u65e0\u6cd5\u8fde\u63a5\u3002\u8bf7\u68c0\u67e5\u60a8\u7684 forked-daapd \u7f51\u7edc\u6743\u9650\u3002", + "websocket_not_enabled": "\u672a\u542f\u7528 forked-daapd \u670d\u52a1\u5668\u7684 Websocket \u529f\u80fd\u3002", + "wrong_server_type": "forked-daapd \u96c6\u6210\u9700\u8981 forked-daapd \u670d\u52a1\u5668\u7248\u672c\u53f7\u81f3\u5c11\u5927\u4e8e\u6216\u7b49\u4e8e 27.0 \u3002" + }, + "step": { + "user": { + "title": "\u8bbe\u7f6e forked-daapd \u8bbe\u5907" + } + } + }, + "options": { + "step": { + "init": { + "description": "\u4e3a forked-daapd \u96c6\u6210\u8bbe\u7f6e\u5404\u79cd\u9009\u9879\u3002", + "title": "\u914d\u7f6e forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 31ac8c2cad9..7a1e1037ddb 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,4 +1,6 @@ """This component provides basic support for Foscam IP cameras.""" +from __future__ import annotations + import asyncio from libpyfoscam import FoscamCamera @@ -172,7 +174,9 @@ class HassFoscamCamera(Camera): """Return the entity unique ID.""" return self._unique_id - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index e68f7208538..939c53b47db 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -109,12 +109,12 @@ class FreeboxSensor(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit.""" return self._unit diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 1f0b848d3b6..c929d56f38e 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -9,6 +9,10 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "link": { + "description": "Kattintson a \u201eK\u00fcld\u00e9s\u201d gombra, majd \u00e9rintse meg a jobbra mutat\u00f3 nyilat az \u00fatv\u00e1laszt\u00f3n a Freebox regisztr\u00e1l\u00e1s\u00e1hoz a HomeAssistant seg\u00edts\u00e9g\u00e9vel. \n\n ! [A gomb helye az \u00fatv\u00e1laszt\u00f3n] (/static/images/config_freebox.png)", + "title": "Freebox \u00fatv\u00e1laszt\u00f3 linkel\u00e9se" + }, "user": { "data": { "host": "Hoszt", diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 0c12f20849c..e5322924864 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -64,8 +64,8 @@ class Device(CoordinatorEntity, SensorEntity): } self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] self._attr_state_class = STATE_CLASS_MAP[device["type"]] - self._attr_unit_of_measurement = UNIT_MAP[device["type"]] - self._attr_state = 0 + self._attr_native_unit_of_measurement = UNIT_MAP[device["type"]] + self._attr_native_value = 0 @callback def _handle_coordinator_update(self) -> None: @@ -80,7 +80,7 @@ class Device(CoordinatorEntity, SensorEntity): ) if device is not None and "state" in device: state = device["state"] - self._attr_state = state[DEVICE_KEY_MAP[self._type]] + self._attr_native_value = state[DEVICE_KEY_MAP[self._type]] super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 8b3f9106602..4ae8314113f 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -6,6 +6,8 @@ PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] DATA_FRITZ = "fritz_data" +DSL_CONNECTION = "dsl" + DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_HOST = "192.168.178.1" DEFAULT_PORT = 49000 diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index faf2be23164..e3d366e83fd 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -8,20 +8,25 @@ from typing import Callable, TypedDict from fritzconnection.core.exceptions import FritzConnectionException from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_GIGABYTES, DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, + SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from .common import FritzBoxBaseEntity, FritzBoxTools -from .const import DOMAIN, UPTIME_DEVIATION +from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) @@ -81,12 +86,50 @@ 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] + + +def _retrieve_link_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: + """Return upload link rate.""" + return round(status.max_linked_bit_rate[0] / 1000, 1) # type: ignore[no-any-return] + + +def _retrieve_link_kb_s_received_state(status: FritzStatus, last_value: str) -> float: + """Return download link rate.""" + return round(status.max_linked_bit_rate[1] / 1000, 1) # type: ignore[no-any-return] + + +def _retrieve_link_noise_margin_sent_state( + status: FritzStatus, last_value: str +) -> float: + """Return upload noise margin.""" + return status.noise_margin[0] # type: ignore[no-any-return] + + +def _retrieve_link_noise_margin_received_state( + status: FritzStatus, last_value: str +) -> float: + """Return download noise margin.""" + return status.noise_margin[1] # type: ignore[no-any-return] + + +def _retrieve_link_attenuation_sent_state( + status: FritzStatus, last_value: str +) -> float: + """Return upload line attenuation.""" + return status.attenuation[0] # type: ignore[no-any-return] + + +def _retrieve_link_attenuation_received_state( + status: FritzStatus, last_value: str +) -> float: + """Return download line attenuation.""" + return status.attenuation[1] # type: ignore[no-any-return] class SensorData(TypedDict, total=False): @@ -95,10 +138,10 @@ class SensorData(TypedDict, total=False): name: str device_class: str | None state_class: str | None - last_reset: bool unit_of_measurement: str | None icon: str | None state_provider: Callable + connection_type: str | None SENSOR_DATA = { @@ -118,47 +161,87 @@ SENSOR_DATA = { state_provider=_retrieve_connection_uptime_state, ), "kb_s_sent": SensorData( - name="kB/s sent", + name="Upload Throughput", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_kb_s_sent_state, ), "kb_s_received": SensorData( - name="kB/s received", + name="Download Throughput", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", state_provider=_retrieve_kb_s_received_state, ), "max_kb_s_sent": SensorData( - name="Max kbit/s sent", + name="Max Connection Upload Throughput", unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_max_kb_s_sent_state, ), "max_kb_s_received": SensorData( - name="Max kbit/s received", + name="Max Connection Download Throughput", unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", state_provider=_retrieve_max_kb_s_received_state, ), "gb_sent": SensorData( name="GB sent", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=True, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=DATA_GIGABYTES, icon="mdi:upload", state_provider=_retrieve_gb_sent_state, ), "gb_received": SensorData( name="GB received", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=True, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=DATA_GIGABYTES, icon="mdi:download", state_provider=_retrieve_gb_received_state, ), + "link_kb_s_sent": SensorData( + name="Link Upload Throughput", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, + icon="mdi:upload", + state_provider=_retrieve_link_kb_s_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_kb_s_received": SensorData( + name="Link Download Throughput", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, + icon="mdi:download", + state_provider=_retrieve_link_kb_s_received_state, + connection_type=DSL_CONNECTION, + ), + "link_noise_margin_sent": SensorData( + name="Link Upload Noise Margin", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:upload", + state_provider=_retrieve_link_noise_margin_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_noise_margin_received": SensorData( + name="Link Download Noise Margin", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:download", + state_provider=_retrieve_link_noise_margin_received_state, + connection_type=DSL_CONNECTION, + ), + "link_attenuation_sent": SensorData( + name="Link Upload Power Attenuation", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:upload", + state_provider=_retrieve_link_attenuation_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_attenuation_received": SensorData( + name="Link Download Power Attenuation", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:download", + state_provider=_retrieve_link_attenuation_received_state, + connection_type=DSL_CONNECTION, + ), } @@ -177,7 +260,16 @@ async def async_setup_entry( return entities = [] - for sensor_type in SENSOR_DATA: + dslinterface = await hass.async_add_executor_job( + fritzbox_tools.connection.call_action, + "WANDSLInterfaceConfig:1", + "GetInfo", + ) + dsl: bool = dslinterface["NewEnable"] + + for sensor_type, sensor_data in SENSOR_DATA.items(): + if not dsl and sensor_data.get("connection_type") == DSL_CONNECTION: + continue entities.append(FritzBoxSensor(fritzbox_tools, entry.title, sensor_type)) if entities: @@ -193,13 +285,14 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Init FRITZ!Box connectivity class.""" self._sensor_data: SensorData = SENSOR_DATA[sensor_type] self._last_device_value: str | None = None - self._last_wan_value: str | None = None self._attr_available = True self._attr_device_class = self._sensor_data.get("device_class") self._attr_icon = self._sensor_data.get("icon") self._attr_name = f"{device_friendly_name} {self._sensor_data['name']}" self._attr_state_class = self._sensor_data.get("state_class") - self._attr_unit_of_measurement = self._sensor_data.get("unit_of_measurement") + self._attr_native_unit_of_measurement = self._sensor_data.get( + "unit_of_measurement" + ) self._attr_unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" super().__init__(fritzbox_tools, device_friendly_name) @@ -220,15 +313,6 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): self._attr_available = False return - self._attr_state = self._last_device_value = self._state_provider( + self._attr_native_value = self._last_device_value = self._state_provider( status, self._last_device_value ) - - if self._sensor_data.get("last_reset") is True: - self._last_wan_value = _retrieve_connection_uptime_state( - status, self._last_wan_value - ) - self._attr_last_reset = datetime.datetime.strptime( - self._last_wan_value, - "%Y-%m-%dT%H:%M:%S%z", - ) 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) diff --git a/homeassistant/components/fritz/translations/zh-Hans.json b/homeassistant/components/fritz/translations/zh-Hans.json new file mode 100644 index 00000000000..91d68989675 --- /dev/null +++ b/homeassistant/components/fritz/translations/zh-Hans.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "start_config": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "\u914d\u7f6e FRITZ!Box Tool \u4ee5\u63a7\u5236\u60a8\u7684 FRITZ!Box\u3002\n\u6700\u4f4e\u4fe1\u606f\u63d0\u4f9b\u8981\u6c42\uff1a\u7528\u6237\u540d\u3001\u5bc6\u7801\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index cef325a61f3..ce5e74cfeec 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, @@ -139,7 +138,6 @@ class FritzBoxEntity(CoordinatorEntity): self.ain = ain self._name = entity_info[ATTR_NAME] self._unique_id = entity_info[ATTR_ENTITY_ID] - self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] self._device_class = entity_info[ATTR_DEVICE_CLASS] self._attr_state_class = entity_info[ATTR_STATE_CLASS] @@ -174,11 +172,6 @@ class FritzBoxEntity(CoordinatorEntity): """Return the name of the device.""" return self._name - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self._unit_of_measurement - @property def device_class(self) -> str | None: """Return the device class.""" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 9d78afca4de..09a652d64ad 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,11 +1,12 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" from __future__ import annotations -from datetime import datetime +from pyfritzhome import FritzhomeDevice from homeassistant.components.sensor import ( ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -25,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import FritzBoxEntity from .const import ( @@ -34,7 +35,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) -from .model import SensorExtraAttributes +from .model import EntityInfo, SensorExtraAttributes async def async_setup_entry( @@ -96,7 +97,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: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, coordinator, ain, @@ -106,48 +107,56 @@ async def async_setup_entry( async_add_entities(entities) -class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): +class FritzBoxSensor(FritzBoxEntity, SensorEntity): + """The entity class for FRITZ!SmartHome sensors.""" + + def __init__( + self, + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + ain: str, + ) -> None: + """Initialize the FritzBox entity.""" + FritzBoxEntity.__init__(self, entity_info, coordinator, ain) + self._attr_native_unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] + + +class FritzBoxBatterySensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome battery sensors.""" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self.device.battery_level # type: ignore [no-any-return] -class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): +class FritzBoxPowerSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome power consumption sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if power := self.device.power: return power / 1000 # type: ignore [no-any-return] return 0.0 -class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): +class FritzBoxEnergySensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome total energy sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if energy := self.device.energy: return energy / 1000 # type: ignore [no-any-return] return 0.0 - @property - def last_reset(self) -> datetime: - """Return the time when the sensor was last reset, if any.""" - # device does not provide timestamp of initialization - return utc_from_timestamp(0) - -class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): +class FritzBoxTempSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome temperature sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" return self.device.temperature # type: ignore [no-any-return] diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 81639b1d830..50a81601310 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -4,6 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Csatlakoztatva az AVM FRITZ! Boxhoz, de nem tudja vez\u00e9relni az intelligens otthoni eszk\u00f6z\u00f6ket.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -30,7 +31,8 @@ "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Adja meg az AVM FRITZ! Box adatait." } } } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 63b3cd81aa5..31e04077656 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -158,7 +158,7 @@ class FritzBoxCallSensor(SensorEntity): return self._fritzbox_phonebook is not None @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 1ae95d30fd5..a8e9c44805d 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -2,7 +2,7 @@ "domain": "fronius", "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.5.3"], + "requirements": ["pyfronius==0.5.5"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 6f949334d02..0fb046e8aa1 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, - SensorEntityDescription, ) from homeassistant.const import ( CONF_DEVICE, @@ -20,14 +20,19 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_SCAN_INTERVAL, CONF_SENSOR_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ) 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__) @@ -48,6 +53,28 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] +PREFIX_DEVICE_CLASS_MAPPING = [ + ("state_of_charge", DEVICE_CLASS_BATTERY), + ("temperature", DEVICE_CLASS_TEMPERATURE), + ("power_factor", DEVICE_CLASS_POWER_FACTOR), + ("power", DEVICE_CLASS_POWER), + ("energy", DEVICE_CLASS_ENERGY), + ("current", DEVICE_CLASS_CURRENT), + ("timestamp", DEVICE_CLASS_TIMESTAMP), + ("voltage", DEVICE_CLASS_VOLTAGE), +] + +PREFIX_STATE_CLASS_MAPPING = [ + ("state_of_charge", STATE_CLASS_MEASUREMENT), + ("temperature", STATE_CLASS_MEASUREMENT), + ("power_factor", STATE_CLASS_MEASUREMENT), + ("power", STATE_CLASS_MEASUREMENT), + ("energy", STATE_CLASS_TOTAL_INCREASING), + ("current", STATE_CLASS_MEASUREMENT), + ("timestamp", STATE_CLASS_MEASUREMENT), + ("voltage", STATE_CLASS_MEASUREMENT), +] + def _device_id_validator(config): """Ensure that inverters have default id 1 and other devices 0.""" @@ -161,12 +188,6 @@ 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: @@ -223,18 +244,6 @@ class FroniusAdapter: 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() @@ -243,18 +252,6 @@ 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) @@ -271,18 +268,6 @@ 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() @@ -291,18 +276,6 @@ 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) @@ -311,14 +284,6 @@ 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() @@ -327,13 +292,19 @@ class FroniusPowerFlow(FroniusAdapter): class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" - def __init__(self, parent: FroniusAdapter, key): + def __init__(self, parent: FroniusAdapter, key: str) -> None: """Initialize a singular value sensor.""" 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 + for prefix, device_class in PREFIX_DEVICE_CLASS_MAPPING: + if self._key.startswith(prefix): + self._attr_device_class = device_class + break + for prefix, state_class in PREFIX_STATE_CLASS_MAPPING: + if self._key.startswith(prefix): + self._attr_state_class = state_class + break @property def should_poll(self): @@ -348,10 +319,10 @@ class FroniusTemplateSensor(SensorEntity): async def async_update(self): """Update the internal state.""" 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") + self._attr_native_value = state.get("value") + if isinstance(self._attr_native_value, float): + self._attr_native_value = round(self._attr_native_value, 2) + self._attr_native_unit_of_measurement = state.get("unit") async def async_added_to_hass(self): """Register at parent component for updates.""" diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b9a84cbec02..a83e3572828 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==20210818.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/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index ed01862aba4..da3a7a4dc24 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -76,7 +76,7 @@ class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return getattr(self.coordinator.data[self._garage_name], self._info_type) @@ -86,7 +86,7 @@ class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): return SENSORS[self._info_type] @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return "cars" diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index 2e4759088fc..8b4c60046db 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -105,7 +105,7 @@ class GdacsSensor(SensorEntity): self._removed = status_info.removed @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._total @@ -125,7 +125,7 @@ class GdacsSensor(SensorEntity): return DEFAULT_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEFAULT_UNIT_OF_MEASUREMENT diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 56b490e165a..b6e08ea8582 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,4 +1,6 @@ """Support for IP Cameras.""" +from __future__ import annotations + import asyncio import logging @@ -118,13 +120,17 @@ class GenericCamera(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: url = self._still_image_url.async_render(parse_result=False) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 0c96ec595b6..362e729f57a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -79,12 +79,12 @@ class GeniusBattery(GeniusDevice, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return PERCENTAGE @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 @@ -105,7 +105,7 @@ class GeniusIssue(GeniusEntity, SensorEntity): self._issues = [] @property - def state(self) -> str: + def native_value(self) -> str: """Return the number of issues.""" return len(self._issues) diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index df5f11850fd..f5797121603 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -121,12 +121,12 @@ class GeoRssServiceSensor(SensorEntity): return f"{self._service_name} {'Any' if self._category is None else self._category}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index 94c7965663a..605f56b1272 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -106,7 +106,7 @@ class GeonetnzQuakesSensor(SensorEntity): self._removed = status_info.removed @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._total @@ -126,7 +126,7 @@ class GeonetnzQuakesSensor(SensorEntity): return DEFAULT_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEFAULT_UNIT_OF_MEASUREMENT diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json index 21a38c18e28..d6070db4fe7 100644 --- a/homeassistant/components/geonetnz_quakes/translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/translations/hu.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Sug\u00e1r" }, "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index c0cc6801437..fc9f0f30b2c 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -130,7 +130,7 @@ class GeonetnzVolcanoSensor(SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._alert_level @@ -145,7 +145,7 @@ class GeonetnzVolcanoSensor(SensorEntity): return f"Volcano {self._title}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "alert level" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 9b890442166..4f19b0d8a68 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -5,7 +5,16 @@ from datetime import timedelta from typing import Final from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CO, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_SULPHUR_DIOXIDE, +) from .model import GiosSensorEntityDescription @@ -36,48 +45,56 @@ SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( GiosSensorEntityDescription( key=ATTR_AQI, name="AQI", + device_class=DEVICE_CLASS_AQI, value=None, ), GiosSensorEntityDescription( key=ATTR_C6H6, name="C6H6", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:molecule", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_CO, name="CO", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_NO2, name="NO2", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_O3, name="O3", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM10, name="PM10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM25, name="PM2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_SO2, name="SO2", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index b651112b9db..9ba5e5410b0 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -86,7 +86,6 @@ class GiosSensor(CoordinatorEntity, SensorEntity): "manufacturer": MANUFACTURER, "entry_type": "service", } - self._attr_icon = "mdi:blur" self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { @@ -107,7 +106,7 @@ class GiosSensor(CoordinatorEntity, SensorEntity): return self._attrs @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" state = getattr(self.coordinator.data, self.entity_description.key).value assert self.entity_description.value is not None @@ -118,7 +117,7 @@ class GiosAqiSensor(GiosSensor): """Define an GIOS AQI sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return cast( StateType, getattr(self.coordinator.data, self.entity_description.key).value diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index d4405196b7a..40693244b91 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,6 +3,6 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": ["PyGithub==1.43.8"], - "codeowners": [], + "codeowners": ["@timmo001"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index c7812fa621d..fb7a0167d8a 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -108,7 +108,7 @@ class GitHubSensor(SensorEntity): return self._unique_id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 0b619853348..e63e07d6c85 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -88,7 +88,7 @@ class GitLabSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 20b68b2e5a9..9e13e155f27 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -65,12 +65,12 @@ class GitterSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index b74662db22b..491dd297a05 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -44,154 +44,154 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( key="disk_use_percent", type="fs", name_suffix="used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="disk_use", type="fs", name_suffix="used", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="disk_free", type="fs", name_suffix="free", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="memory_use_percent", type="mem", name_suffix="RAM used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), GlancesSensorEntityDescription( key="memory_use", type="mem", name_suffix="RAM used", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="memory_free", type="mem", name_suffix="RAM free", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_use_percent", type="memswap", name_suffix="Swap used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_use", type="memswap", name_suffix="Swap used", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_free", type="memswap", name_suffix="Swap free", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="processor_load", type="load", name_suffix="CPU load", - unit_of_measurement="15 min", + native_unit_of_measurement="15 min", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_running", type="processcount", name_suffix="Running", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_total", type="processcount", name_suffix="Total", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_thread", type="processcount", name_suffix="Thread", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_sleeping", type="processcount", name_suffix="Sleeping", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="cpu_use_percent", type="cpu", name_suffix="CPU used", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=CPU_ICON, ), GlancesSensorEntityDescription( key="temperature_core", type="sensors", name_suffix="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), GlancesSensorEntityDescription( key="temperature_hdd", type="sensors", name_suffix="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), GlancesSensorEntityDescription( key="fan_speed", type="sensors", name_suffix="Fan speed", - unit_of_measurement="RPM", + native_unit_of_measurement="RPM", icon="mdi:fan", ), GlancesSensorEntityDescription( key="battery", type="sensors", name_suffix="Charge", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", ), GlancesSensorEntityDescription( key="docker_active", type="docker", name_suffix="Containers active", - unit_of_measurement="", + native_unit_of_measurement="", icon="mdi:docker", ), GlancesSensorEntityDescription( key="docker_cpu_use", type="docker", name_suffix="Containers CPU used", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:docker", ), GlancesSensorEntityDescription( key="docker_memory_use", type="docker", name_suffix="Containers RAM used", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:docker", ), ) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index fd31ee37faf..76e2a1c617a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -83,7 +83,7 @@ class GlancesSensor(SensorEntity): return self.glances_data.available @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/glances/translations/zh-Hans.json b/homeassistant/components/glances/translations/zh-Hans.json index 22cb2995672..a62b5f8b32e 100644 --- a/homeassistant/components/glances/translations/zh-Hans.json +++ b/homeassistant/components/glances/translations/zh-Hans.json @@ -1,15 +1,35 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u8fde\u63a5" + }, "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "wrong_version": "\u4e0d\u652f\u6301\u7684\u7248\u672c (\u4ec5\u96502\u62163)" }, "step": { "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", "name": "\u540d\u79f0", "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u51ed\u8bc1", + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66", + "version": "Glances API \u7248\u672c (2 \u6216 3)" + }, + "title": "\u8bbe\u7f6e Glances" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9891\u7387" + }, + "description": "\u914d\u7f6e Glances \u9009\u9879" } } } diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 308934819cd..379a56512c6 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SENSOR, DOMAIN_SWITCH] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Goal Zero Yeti from a config entry.""" name = entry.data[CONF_NAME] host = entry.data[CONF_HOST] @@ -81,7 +81,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: @@ -94,7 +94,13 @@ class YetiEntity(CoordinatorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - def __init__(self, api, coordinator, name, server_unique_id): + def __init__( + self, + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) self.api = api @@ -104,15 +110,10 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - model = sw_version = None - if self.api.sysdata: - model = self.api.sysdata[ATTR_MODEL] - if self.api.data: - sw_version = self.api.data["firmwareVersion"] return { ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, ATTR_MANUFACTURER: "Goal Zero", ATTR_NAME: self._name, - ATTR_MODEL: str(model), - ATTR_SW_VERSION: str(sw_version), + ATTR_MODEL: self.api.sysdata.get(ATTR_MODEL), + ATTR_SW_VERSION: self.api.data.get("firmwareVersion"), } diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index f9a110eff55..21eecc678ad 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,14 +1,51 @@ """Support for Goal Zero Yeti Sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, CONF_NAME +from __future__ import annotations -from . import YetiEntity -from .const import BINARY_SENSOR_DICT, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN PARALLEL_UPDATES = 0 +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="backlight", + name="Backlight", + icon="mdi:clock-digital", + ), + BinarySensorEntityDescription( + key="app_online", + name="App Online", + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + BinarySensorEntityDescription( + key="isCharging", + name="Charging", + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ), + BinarySensorEntityDescription( + key="inputDetected", + name="Input Detected", + device_class=DEVICE_CLASS_POWER, + ), +) -async def async_setup_entry(hass, entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Goal Zero Yeti sensor.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -17,10 +54,10 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - sensor_name, + description, entry.entry_id, ) - for sensor_name in BINARY_SENSOR_DICT + for description in BINARY_SENSOR_TYPES ) @@ -29,26 +66,19 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): def __init__( self, - api, - coordinator, - name, - sensor_name, - server_unique_id, - ): + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: BinarySensorEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) - - self._condition = sensor_name - self._attr_device_class = BINARY_SENSOR_DICT[sensor_name].get(ATTR_DEVICE_CLASS) - self._attr_icon = BINARY_SENSOR_DICT[sensor_name].get(ATTR_ICON) - self._attr_name = f"{name} {BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" - self._attr_unique_id = ( - f"{server_unique_id}/{BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" - ) + self.entity_description = description + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property def is_on(self) -> bool: """Return if the service is on.""" - if self.api.data: - return self.api.data[self._condition] == 1 - return False + return self.api.data.get(self.entity_description.key) == 1 diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 4c525de9c7d..cc2c4a9874f 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DEFAULT_NAME, DOMAIN @@ -24,11 +25,11 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize a Goal Zero Yeti flow.""" self.ip_address = None - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info[IP_ADDRESS] @@ -36,7 +37,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) self._async_abort_entries_match({CONF_HOST: self.ip_address}) - _, error = await self._async_try_connect(self.ip_address) + _, error = await self._async_try_connect(str(self.ip_address)) if error is None: return await self.async_step_confirm_discovery() return self.async_abort(reason=error) @@ -63,7 +64,9 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -74,7 +77,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address, error = await self._async_try_connect(host) if error is None: - await self.async_set_unique_id(format_mac(mac_address)) + await self.async_set_unique_id(format_mac(str(mac_address))) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=name, @@ -98,7 +101,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_try_connect(self, host) -> tuple: + async def _async_try_connect(self, host: str) -> tuple[str | None, str | None]: """Try connecting to Goal Zero Yeti.""" try: session = async_get_clientsession(self.hass) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index e9fed7dc52b..d99cacb253e 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -1,37 +1,6 @@ """Constants for the Goal Zero Yeti integration.""" from datetime import timedelta -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_POWER, -) -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_WATT_HOUR, - PERCENTAGE, - POWER_WATT, - SIGNAL_STRENGTH_DECIBELS, - TEMP_CELSIUS, - TIME_MINUTES, - TIME_SECONDS, -) - ATTRIBUTION = "Data provided by Goal Zero" ATTR_DEFAULT_ENABLED = "default_enabled" @@ -41,113 +10,3 @@ DEFAULT_NAME = "Yeti" DATA_KEY_API = "api" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -BINARY_SENSOR_DICT = { - "backlight": {ATTR_NAME: "Backlight", ATTR_ICON: "mdi:clock-digital"}, - "app_online": { - ATTR_NAME: "App Online", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, - }, - "isCharging": { - ATTR_NAME: "Charging", - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - }, - "inputDetected": { - ATTR_NAME: "Input Detected", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - }, -} - -SENSOR_DICT = { - "wattsIn": { - ATTR_NAME: "Watts In", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "ampsIn": { - ATTR_NAME: "Amps In", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "wattsOut": { - ATTR_NAME: "Watts Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "ampsOut": { - ATTR_NAME: "Amps Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "whOut": { - ATTR_NAME: "WH Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "whStored": { - ATTR_NAME: "WH Stored", - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "volts": { - ATTR_NAME: "Volts", - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEFAULT_ENABLED: False, - }, - "socPercent": { - ATTR_NAME: "State of Charge Percent", - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEFAULT_ENABLED: True, - }, - "timeToEmptyFull": { - ATTR_NAME: "Time to Empty/Full", - ATTR_DEVICE_CLASS: TIME_MINUTES, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, - ATTR_DEFAULT_ENABLED: True, - }, - "temperature": { - ATTR_NAME: "Temperature", - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEFAULT_ENABLED: True, - }, - "wifiStrength": { - ATTR_NAME: "Wifi Strength", - ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, - ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, - ATTR_DEFAULT_ENABLED: True, - }, - "timestamp": { - ATTR_NAME: "Total Run Time", - ATTR_UNIT_OF_MEASUREMENT: TIME_SECONDS, - ATTR_DEFAULT_ENABLED: False, - }, - "ssid": { - ATTR_NAME: "Wi-Fi SSID", - ATTR_DEFAULT_ENABLED: False, - }, - "ipAddr": { - ATTR_NAME: "IP Address", - ATTR_DEFAULT_ENABLED: False, - }, -} - -SWITCH_DICT = { - "v12PortStatus": "12V Port Status", - "usbPortStatus": "USB Port Status", - "acPortStatus": "AC Port Status", -} diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 594e1f0046b..8890c7db69c 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,25 +1,138 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations -from homeassistant.components.sensor import ATTR_LAST_RESET, ATTR_STATE_CLASS +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_WATT_HOUR, + PERCENTAGE, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, + TIME_MINUTES, + TIME_SECONDS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import ( - ATTR_DEFAULT_ENABLED, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN, - SENSOR_DICT, +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="wattsIn", + name="Watts In", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ampsIn", + name="Amps In", + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="wattsOut", + name="Watts Out", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ampsOut", + name="Amps Out", + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="whOut", + name="WH Out", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="whStored", + name="WH Stored", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="volts", + name="Volts", + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="socPercent", + name="State of Charge Percent", + device_class=DEVICE_CLASS_BATTERY, + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="timeToEmptyFull", + name="Time to Empty/Full", + device_class=TIME_MINUTES, + unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + ), + SensorEntityDescription( + key="wifiStrength", + name="Wifi Strength", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + ), + SensorEntityDescription( + key="timestamp", + name="Total Run Time", + unit_of_measurement=TIME_SECONDS, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="ssid", + name="Wi-Fi SSID", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="ipAddr", + name="IP Address", + entity_registry_enabled_default=False, + ), ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Goal Zero Yeti sensor.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -28,33 +141,32 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - sensor_name, + description, entry.entry_id, ) - for sensor_name in SENSOR_DICT + for description in SENSOR_TYPES ] async_add_entities(sensors, True) -class YetiSensor(YetiEntity): +class YetiSensor(YetiEntity, SensorEntity): """Representation of a Goal Zero Yeti sensor.""" - def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + def __init__( + self, + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: SensorEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = sensor_name - sensor = SENSOR_DICT[sensor_name] - self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) - self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) - self._attr_last_reset = sensor.get(ATTR_LAST_RESET) - self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" - self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{server_unique_id}/{sensor_name}" - self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) + self._attr_name = f"{name} {description.name}" + self.entity_description = description + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property - def state(self) -> str | None: + def native_value(self) -> str: """Return the state.""" - if self.api.data: - return self.api.data.get(self._condition) - return None + return self.api.data.get(self.entity_description.key) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9d37bcb0b7b..767c728e62b 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,14 +1,35 @@ """Support for Goal Zero Yeti Switches.""" from __future__ import annotations -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, SWITCH_DICT +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="v12PortStatus", + name="12V Port Status", + ), + SwitchEntityDescription( + key="usbPortStatus", + name="USB Port Status", + ), + SwitchEntityDescription( + key="acPortStatus", + name="AC Port Status", + ), +) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Goal Zero Yeti switch.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -17,10 +38,10 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - switch_name, + description, entry.entry_id, ) - for switch_name in SWITCH_DICT + for description in SWITCH_TYPES ) @@ -29,33 +50,31 @@ class YetiSwitch(YetiEntity, SwitchEntity): def __init__( self, - api, - coordinator, - name, - switch_name, - server_unique_id, - ): + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: SwitchEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti switch.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = switch_name - self._attr_name = f"{name} {SWITCH_DICT[switch_name]}" - self._attr_unique_id = f"{server_unique_id}/{switch_name}" + self.entity_description = description + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property def is_on(self) -> bool: """Return state of the switch.""" - if self.api.data: - return self.api.data[self._condition] - return False + return self.api.data.get(self.entity_description.key) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn off the switch.""" - payload = {self._condition: 0} + payload = {self.entity_description.key: 0} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn on the switch.""" - payload = {self._condition: 1} + payload = {self.entity_description.key: 1} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 99edc855733..a9be18d06a6 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -72,7 +72,7 @@ class DoorSensorBattery(GoGoGate2Entity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the entity.""" door = self._get_door() return door.voltage # This is a percentage, not an absolute voltage @@ -110,13 +110,13 @@ class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def state(self): + def native_value(self): """Return the state of the entity.""" door = self._get_door() return door.temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement.""" return TEMP_CELSIUS diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json index 641046d7745..30d6ef5c016 100644 --- a/homeassistant/components/gogogate2/translations/hu.json +++ b/homeassistant/components/gogogate2/translations/hu.json @@ -15,6 +15,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Adja meg a sz\u00fcks\u00e9ges inform\u00e1ci\u00f3kat al\u00e1bb.", "title": "A GogoGate2 vagy az iSmartGate be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 6cc7221ba1d..33afac6f57b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -108,16 +108,19 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_SINGLE_CALSEARCH_CONFIG = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, - vol.Optional(CONF_OFFSET): cv.string, - vol.Optional(CONF_SEARCH): cv.string, - vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_MAX_RESULTS): cv.positive_int, - } +_SINGLE_CALSEARCH_CONFIG = vol.All( + cv.deprecated(CONF_MAX_RESULTS), + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, + vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_SEARCH): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, + vol.Optional(CONF_MAX_RESULTS): cv.positive_int, # Now unused + } + ), ) DEVICE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 2cc66121948..5c06e0fbb94 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -18,7 +18,6 @@ from homeassistant.util import Throttle, dt from . import ( CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, - CONF_MAX_RESULTS, CONF_SEARCH, CONF_TRACK, DEFAULT_CONF_OFFSET, @@ -30,7 +29,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_GOOGLE_SEARCH_PARAMS = { "orderBy": "startTime", - "maxResults": 5, "singleEvents": True, } @@ -71,7 +69,6 @@ class GoogleCalendarEventDevice(CalendarEventDevice): calendar, data.get(CONF_SEARCH), data.get(CONF_IGNORE_AVAILABILITY), - data.get(CONF_MAX_RESULTS), ) self._event = None self._name = data[CONF_NAME] @@ -113,15 +110,12 @@ class GoogleCalendarEventDevice(CalendarEventDevice): class GoogleCalendarData: """Class to utilize calendar service object to get next event.""" - def __init__( - self, calendar_service, calendar_id, search, ignore_availability, max_results - ): + def __init__(self, calendar_service, calendar_id, search, ignore_availability): """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id self.search = search self.ignore_availability = ignore_availability - self.max_results = max_results self.event = None def _prepare_query(self): @@ -132,8 +126,8 @@ class GoogleCalendarData: return None, None params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params["calendarId"] = self.calendar_id - if self.max_results: - params["maxResults"] = self.max_results + params["maxResults"] = 100 # Page size + if self.search: params["q"] = self.search @@ -147,18 +141,30 @@ class GoogleCalendarData: params["timeMin"] = start_date.isoformat("T") params["timeMax"] = end_date.isoformat("T") + event_list = [] events = await hass.async_add_executor_job(service.events) + page_token = None + while True: + page_token = await self.async_get_events_page( + hass, events, params, page_token, event_list + ) + if not page_token: + break + return event_list + + async def async_get_events_page(self, hass, events, params, page_token, event_list): + """Get a page of events in a specific time frame.""" + params["pageToken"] = page_token result = await hass.async_add_executor_job(events.list(**params).execute) items = result.get("items", []) - event_list = [] for item in items: if not self.ignore_availability and "transparency" in item: if item["transparency"] == "opaque": event_list.append(item) else: event_list.append(item) - return event_list + return result.get("nextPageToken") @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 13516783233..1e0c0a06114 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -2,14 +2,13 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol -# Typing imports from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ALIASES, @@ -91,7 +90,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Google Actions component.""" if DOMAIN not in yaml_config: return True diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 36222902296..06d10c5372b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1290,7 +1290,7 @@ class FanSpeedTrait(_Trait): ) elif domain == climate.DOMAIN: - modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) + modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or [] for mode in modes: speed = { "speed_name": mode, diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index b6bd6f71bf4..1a0396a69ac 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -1,4 +1,6 @@ """Support for Google Maps location sharing.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -6,7 +8,10 @@ from locationsharinglib import Service from locationsharinglib.locationsharinglibexceptions import InvalidCookies import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, + SOURCE_TYPE_GPS, +) from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -30,7 +35,9 @@ CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" CREDENTIALS_FILE = ".google_maps_location_sharing.cookies" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +# the parent "device_tracker" have marked the schemas as legacy, so this +# need to be refactored as part of a bigger rewrite. +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), @@ -53,7 +60,7 @@ class GoogleMapsScanner: self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) - self._prev_seen = {} + self._prev_seen: dict[str, str] = {} credfile = f"{hass.config.path(CREDENTIALS_FILE)}.{slugify(self.username)}" try: diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 514b919e877..1de7e98d776 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -5,7 +5,6 @@ import datetime import json import logging import os -from typing import Any from google.cloud import pubsub_v1 import voluptuous as vol @@ -14,6 +13,7 @@ from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UN from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -39,15 +39,12 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): +def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Google Pub/Sub component.""" - config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] - service_principal_path = os.path.join( - hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL] - ) + service_principal_path = hass.config.path(config[CONF_SERVICE_PRINCIPAL]) if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6dbe6aa698b..c8cb9d54510 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -196,7 +196,7 @@ class GoogleTravelTimeSensor(SensorEntity): await self.first_update() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._matrix is None: return None @@ -250,7 +250,7 @@ class GoogleTravelTimeSensor(SensorEntity): return res @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 28ec5df7486..4a062edaae2 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -95,7 +95,7 @@ class GoogleWifiSensor(SensorEntity): return self._var_icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._var_units @@ -105,7 +105,7 @@ class GoogleWifiSensor(SensorEntity): return self._api.available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 2f97f62337c..1b502827996 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -84,7 +84,7 @@ class GpsdSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of GPSD.""" if self.agps_thread.data_stream.mode == 3: return "3D Fix" diff --git a/homeassistant/components/gree/translations/zh-Hans.json b/homeassistant/components/gree/translations/zh-Hans.json new file mode 100644 index 00000000000..808f01b57a8 --- /dev/null +++ b/homeassistant/components/gree/translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6b64\u7f51\u7edc\u672a\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", + "single_instance_allowed": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e\u3002\u53ea\u5141\u8bb8\u5b58\u5728\u4e00\u4e2a\u914d\u7f6e\u6587\u6863" + }, + "step": { + "confirm": { + "description": "\u4f60\u60f3\u8981\u5f00\u59cb\u914d\u7f6e\u5417\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index fac11395c8b..7fbfa717229 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -147,7 +147,7 @@ class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" _attr_icon = CURRENT_SENSOR_ICON - _attr_unit_of_measurement = UNIT_WATTS + _attr_native_unit_of_measurement = UNIT_WATTS def __init__(self, monitor_serial_number, number, name, net_metering): """Construct the entity.""" @@ -158,7 +158,7 @@ class CurrentSensor(GEMSensor): return monitor.channels[self._number - 1] @property - def state(self): + def native_value(self): """Return the current number of watts being used by the channel.""" if not self._sensor: return None @@ -203,7 +203,7 @@ class PulseCounter(GEMSensor): return monitor.pulse_counters[self._number - 1] @property - def state(self): + def native_value(self): """Return the current rate of change for the given pulse counter.""" if not self._sensor or self._sensor.pulses_per_second is None: return None @@ -225,7 +225,7 @@ class PulseCounter(GEMSensor): return 3600 @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement for this pulse counter.""" return f"{self._counted_quantity}/{self._time_unit}" @@ -253,7 +253,7 @@ class TemperatureSensor(GEMSensor): return monitor.temperature_sensors[self._number - 1] @property - def state(self): + def native_value(self): """Return the current temperature being reported by this sensor.""" if not self._sensor: return None @@ -261,7 +261,7 @@ class TemperatureSensor(GEMSensor): return self._sensor.temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement for this sensor (user specified).""" return self._unit @@ -270,7 +270,7 @@ class VoltageSensor(GEMSensor): """Entity showing voltage.""" _attr_icon = VOLTAGE_ICON - _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT + _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT def __init__(self, monitor_serial_number, number, name): """Construct the entity.""" @@ -281,7 +281,7 @@ class VoltageSensor(GEMSensor): return monitor @property - def state(self): + def native_value(self): """Return the current voltage being reported by this sensor.""" if not self._sensor: return None diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index 45f56a327b2..d6b2c7db9fe 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -76,7 +76,3 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data.update(user_input) return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) - - async def async_step_import(self, import_data): - """Migrate old yaml config to config flow.""" - return await self.async_step_user(import_data) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index fe6bdeb70e8..671631c5406 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,14 +1,15 @@ """Read status of growatt inverters.""" +from __future__ import annotations + +from dataclasses import dataclass import datetime import json import logging import re import growattServer -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -31,547 +32,651 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DEFAULT_URL, DOMAIN +from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=1) -# Sensor type order is: Sensor name, Unit of measurement, api data name, additional options -TOTAL_SENSOR_TYPES = { - "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}), - "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}), - "total_energy_today": ( - "Energy Today", - ENERGY_KILO_WATT_HOUR, - "todayEnergy", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "total_output_power": ( - "Output Power", - POWER_WATT, - "invTodayPpv", - {"device_class": DEVICE_CLASS_POWER}, - ), - "total_energy_output": ( - "Lifetime energy output", - ENERGY_KILO_WATT_HOUR, - "totalEnergy", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "total_maximum_output": ( - "Maximum power", - POWER_WATT, - "nominalPower", - {"device_class": DEVICE_CLASS_POWER}, - ), -} -INVERTER_SENSOR_TYPES = { - "inverter_energy_today": ( - "Energy today", - ENERGY_KILO_WATT_HOUR, - "powerToday", - {"round": 1, "device_class": DEVICE_CLASS_ENERGY}, - ), - "inverter_energy_total": ( - "Lifetime energy output", - ENERGY_KILO_WATT_HOUR, - "powerTotal", - {"round": 1, "device_class": DEVICE_CLASS_ENERGY}, - ), - "inverter_voltage_input_1": ( - "Input 1 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv1", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_1": ( - "Input 1 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv1", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_1": ( - "Input 1 Wattage", - POWER_WATT, - "ppv1", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_voltage_input_2": ( - "Input 2 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv2", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_2": ( - "Input 2 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv2", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_2": ( - "Input 2 Wattage", - POWER_WATT, - "ppv2", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_voltage_input_3": ( - "Input 3 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv3", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_3": ( - "Input 3 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv3", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_3": ( - "Input 3 Wattage", - POWER_WATT, - "ppv3", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_internal_wattage": ( - "Internal wattage", - POWER_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_reactive_voltage": ( - "Reactive voltage", - ELECTRIC_POTENTIAL_VOLT, - "vacr", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_inverter_reactive_amperage": ( - "Reactive amperage", - ELECTRIC_CURRENT_AMPERE, - "iacr", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_frequency": ("AC frequency", FREQUENCY_HERTZ, "fac", {"round": 1}), - "inverter_current_wattage": ( - "Output power", - POWER_WATT, - "pac", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_current_reactive_wattage": ( - "Reactive wattage", - POWER_WATT, - "pacr", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_ipm_temperature": ( - "Intelligent Power Management temperature", - TEMP_CELSIUS, - "ipmTemperature", - {"device_class": DEVICE_CLASS_TEMPERATURE, "round": 1}, - ), - "inverter_temperature": ( - "Temperature", - TEMP_CELSIUS, - "temperature", - {"device_class": DEVICE_CLASS_TEMPERATURE, "round": 1}, - ), -} +@dataclass +class GrowattRequiredKeysMixin: + """Mixin for required keys.""" -STORAGE_SENSOR_TYPES = { - "storage_storage_production_today": ( - "Storage production today", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_storage_production_lifetime": ( - "Lifetime Storage production", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_grid_discharge_today": ( - "Grid discharged today", - ENERGY_KILO_WATT_HOUR, - "eacDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption_today": ( - "Load consumption today", - ENERGY_KILO_WATT_HOUR, - "eopDischrToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption_lifetime": ( - "Lifetime load consumption", - ENERGY_KILO_WATT_HOUR, - "eopDischrTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_grid_charged_today": ( - "Grid charged today", - ENERGY_KILO_WATT_HOUR, - "eacChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_charge_storage_lifetime": ( - "Lifetime storaged charged", - ENERGY_KILO_WATT_HOUR, - "eChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_solar_production": ( - "Solar power production", - POWER_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_battery_percentage": ( - "Battery percentage", - PERCENTAGE, - "capacity", - {"device_class": DEVICE_CLASS_BATTERY}, - ), - "storage_power_flow": ( - "Storage charging/ discharging(-ve)", - POWER_WATT, - "pCharge", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_load_consumption_solar_storage": ( - "Load consumption(Solar + Storage)", - "VA", - "rateVA", - {}, - ), - "storage_charge_today": ( - "Charge today", - ENERGY_KILO_WATT_HOUR, - "eChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_import_from_grid": ( - "Import from grid", - POWER_WATT, - "pAcInPut", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_import_from_grid_today": ( - "Import from grid today", - ENERGY_KILO_WATT_HOUR, - "eToUserToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_import_from_grid_total": ( - "Import from grid total", - ENERGY_KILO_WATT_HOUR, - "eToUserTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption": ( - "Load consumption", - POWER_WATT, - "outPutPower", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_grid_voltage": ( - "AC input voltage", - ELECTRIC_POTENTIAL_VOLT, - "vGrid", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_pv_charging_voltage": ( - "PV charging voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_ac_input_frequency_out": ( - "AC input frequency", - FREQUENCY_HERTZ, - "freqOutPut", - {"round": 2}, - ), - "storage_output_voltage": ( - "Output voltage", - ELECTRIC_POTENTIAL_VOLT, - "outPutVolt", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_ac_output_frequency": ( - "Ac output frequency", - FREQUENCY_HERTZ, - "freqGrid", - {"round": 2}, - ), - "storage_current_PV": ( - "Solar charge current", - ELECTRIC_CURRENT_AMPERE, - "iAcCharge", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_current_1": ( - "Solar current to storage", - ELECTRIC_CURRENT_AMPERE, - "iChargePV1", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_grid_amperage_input": ( - "Grid charge current", - ELECTRIC_CURRENT_AMPERE, - "chgCurr", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_grid_out_current": ( - "Grid out current", - ELECTRIC_CURRENT_AMPERE, - "outPutCurrent", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_battery_voltage": ( - "Battery voltage", - ELECTRIC_POTENTIAL_VOLT, - "vBat", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_load_percentage": ( - "Load percentage", - PERCENTAGE, - "loadPercent", - {"device_class": DEVICE_CLASS_BATTERY, "round": 2}, - ), -} + api_key: str -MIX_SENSOR_TYPES = { - # Values from 'mix_info' API call - "mix_statement_of_charge": ( - "Statement of charge", - PERCENTAGE, - "capacity", - {"device_class": DEVICE_CLASS_BATTERY}, - ), - "mix_battery_charge_today": ( - "Battery charged today", - ENERGY_KILO_WATT_HOUR, - "eBatChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_battery_charge_lifetime": ( - "Lifetime battery charged", - ENERGY_KILO_WATT_HOUR, - "eBatChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_battery_discharge_today": ( - "Battery discharged today", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_battery_discharge_lifetime": ( - "Lifetime battery discharged", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_solar_generation_today": ( - "Solar energy today", - ENERGY_KILO_WATT_HOUR, - "epvToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_solar_generation_lifetime": ( - "Lifetime solar energy", - ENERGY_KILO_WATT_HOUR, - "epvTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_battery_discharge_w": ( - "Battery discharging W", - POWER_WATT, - "pDischarge1", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_battery_voltage": ( - "Battery voltage", - ELECTRIC_POTENTIAL_VOLT, - "vbat", - {"device_class": DEVICE_CLASS_VOLTAGE}, - ), - "mix_pv1_voltage": ( - "PV1 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv1", - {"device_class": DEVICE_CLASS_VOLTAGE}, - ), - "mix_pv2_voltage": ( - "PV2 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv2", - {"device_class": DEVICE_CLASS_VOLTAGE}, - ), - # Values from 'mix_totals' API call - "mix_load_consumption_today": ( - "Load consumption today", - ENERGY_KILO_WATT_HOUR, - "elocalLoadToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_load_consumption_lifetime": ( - "Lifetime load consumption", - ENERGY_KILO_WATT_HOUR, - "elocalLoadTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_export_to_grid_today": ( - "Export to grid today", - ENERGY_KILO_WATT_HOUR, - "etoGridToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_export_to_grid_lifetime": ( - "Lifetime export to grid", - ENERGY_KILO_WATT_HOUR, - "etogridTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - # Values from 'mix_system_status' API call - "mix_battery_charge": ( - "Battery charging", - POWER_KILO_WATT, - "chargePower", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_load_consumption": ( - "Load consumption", - POWER_KILO_WATT, - "pLocalLoad", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_wattage_pv_1": ( - "PV1 Wattage", - POWER_KILO_WATT, - "pPv1", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_wattage_pv_2": ( - "PV2 Wattage", - POWER_KILO_WATT, - "pPv2", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_wattage_pv_all": ( - "All PV Wattage", - POWER_KILO_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_export_to_grid": ( - "Export to grid", - POWER_KILO_WATT, - "pactogrid", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_import_from_grid": ( - "Import from grid", - POWER_KILO_WATT, - "pactouser", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_battery_discharge_kw": ( - "Battery discharging kW", - POWER_KILO_WATT, - "pdisCharge1", - {"device_class": DEVICE_CLASS_POWER}, - ), - "mix_grid_voltage": ( - "Grid voltage", - ELECTRIC_POTENTIAL_VOLT, - "vAc1", - {"device_class": DEVICE_CLASS_VOLTAGE}, - ), - # Values from 'mix_detail' API call - "mix_system_production_today": ( - "System production today (self-consumption + export)", - ENERGY_KILO_WATT_HOUR, - "eCharge", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_load_consumption_solar_today": ( - "Load consumption today (solar)", - ENERGY_KILO_WATT_HOUR, - "eChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_self_consumption_today": ( - "Self consumption today (solar + battery)", - ENERGY_KILO_WATT_HOUR, - "eChargeToday1", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_load_consumption_battery_today": ( - "Load consumption today (battery)", - ENERGY_KILO_WATT_HOUR, - "echarge1", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "mix_import_from_grid_today": ( - "Import from grid today (load)", - ENERGY_KILO_WATT_HOUR, - "etouser", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - # This sensor is manually created using the most recent X-Axis value from the chartData - "mix_last_update": ( - "Last Data Update", - None, - "lastdataupdate", - {"device_class": DEVICE_CLASS_TIMESTAMP}, - ), - # Values from 'dashboard_data' API call - "mix_import_from_grid_today_combined": ( - "Import from grid today (load + charging)", - ENERGY_KILO_WATT_HOUR, - "etouser_combined", # This id is not present in the raw API data, it is added by the sensor - {"device_class": DEVICE_CLASS_ENERGY}, - ), -} -SENSOR_TYPES = { - **TOTAL_SENSOR_TYPES, - **INVERTER_SENSOR_TYPES, - **STORAGE_SENSOR_TYPES, - **MIX_SENSOR_TYPES, -} +@dataclass +class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin): + """Describes Growatt sensor entity.""" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL, default=DEFAULT_URL): cv.string, - } + precision: int | None = None + + +TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="total_money_today", + name="Total money today", + api_key="plantMoneyText", + native_unit_of_measurement=CURRENCY_EURO, + ), + GrowattSensorEntityDescription( + key="total_money_total", + name="Money lifetime", + api_key="totalMoneyText", + native_unit_of_measurement=CURRENCY_EURO, + ), + GrowattSensorEntityDescription( + key="total_energy_today", + name="Energy Today", + api_key="todayEnergy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="total_output_power", + name="Output Power", + api_key="invTodayPpv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="total_energy_output", + name="Lifetime energy output", + api_key="totalEnergy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="total_maximum_output", + name="Maximum power", + api_key="nominalPower", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), ) +INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="inverter_energy_today", + name="Energy today", + api_key="powerToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_energy_total", + name="Lifetime energy output", + api_key="powerTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_1", + name="Input 1 voltage", + api_key="vpv1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_1", + name="Input 1 Amperage", + api_key="ipv1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_1", + name="Input 1 Wattage", + api_key="ppv1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_2", + name="Input 2 voltage", + api_key="vpv2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_2", + name="Input 2 Amperage", + api_key="ipv2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_2", + name="Input 2 Wattage", + api_key="ppv2", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_3", + name="Input 3 voltage", + api_key="vpv3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_3", + name="Input 3 Amperage", + api_key="ipv3", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_3", + name="Input 3 Wattage", + api_key="ppv3", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_internal_wattage", + name="Internal wattage", + api_key="ppv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_reactive_voltage", + name="Reactive voltage", + api_key="vacr", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_inverter_reactive_amperage", + name="Reactive amperage", + api_key="iacr", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_frequency", + name="AC frequency", + api_key="fac", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_wattage", + name="Output power", + api_key="pac", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_reactive_wattage", + name="Reactive wattage", + api_key="pacr", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_ipm_temperature", + name="Intelligent Power Management temperature", + api_key="ipmTemperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_temperature", + name="Temperature", + api_key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), +) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up growatt server from yaml.""" - if not hass.config_entries.async_entries(DOMAIN): - _LOGGER.warning( - "Loading Growatt via platform setup is deprecated." - "Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) +STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="storage_storage_production_today", + name="Storage production today", + api_key="eBatDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_storage_production_lifetime", + name="Lifetime Storage production", + api_key="eBatDisChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_grid_discharge_today", + name="Grid discharged today", + api_key="eacDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_today", + name="Load consumption today", + api_key="eopDischrToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="eopDischrTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_grid_charged_today", + name="Grid charged today", + api_key="eacChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_charge_storage_lifetime", + name="Lifetime storaged charged", + api_key="eChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_solar_production", + name="Solar power production", + api_key="ppv", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_battery_percentage", + name="Battery percentage", + api_key="capacity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + GrowattSensorEntityDescription( + key="storage_power_flow", + name="Storage charging/ discharging(-ve)", + api_key="pCharge", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_solar_storage", + name="Load consumption(Solar + Storage)", + api_key="rateVA", + native_unit_of_measurement="VA", + ), + GrowattSensorEntityDescription( + key="storage_charge_today", + name="Charge today", + api_key="eChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid", + name="Import from grid", + api_key="pAcInPut", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_today", + name="Import from grid today", + api_key="eToUserToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_total", + name="Import from grid total", + api_key="eToUserTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption", + name="Load consumption", + api_key="outPutPower", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_grid_voltage", + name="AC input voltage", + api_key="vGrid", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_pv_charging_voltage", + name="PV charging voltage", + api_key="vpv", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_input_frequency_out", + name="AC input frequency", + api_key="freqOutPut", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_output_voltage", + name="Output voltage", + api_key="outPutVolt", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_output_frequency", + name="Ac output frequency", + api_key="freqGrid", + native_unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_PV", + name="Solar charge current", + api_key="iAcCharge", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_1", + name="Solar current to storage", + api_key="iChargePV1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_amperage_input", + name="Grid charge current", + api_key="chgCurr", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_out_current", + name="Grid out current", + api_key="outPutCurrent", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_battery_voltage", + name="Battery voltage", + api_key="vBat", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_load_percentage", + name="Load percentage", + api_key="loadPercent", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + precision=2, + ), +) + +MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + # Values from 'mix_info' API call + GrowattSensorEntityDescription( + key="mix_statement_of_charge", + name="Statement of charge", + api_key="capacity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + GrowattSensorEntityDescription( + key="mix_battery_charge_today", + name="Battery charged today", + api_key="eBatChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_charge_lifetime", + name="Lifetime battery charged", + api_key="eBatChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_today", + name="Battery discharged today", + api_key="eBatDisChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_lifetime", + name="Lifetime battery discharged", + api_key="eBatDisChargeTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_solar_generation_today", + name="Solar energy today", + api_key="epvToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_solar_generation_lifetime", + name="Lifetime solar energy", + api_key="epvTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_w", + name="Battery discharging W", + api_key="pDischarge1", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_battery_voltage", + name="Battery voltage", + api_key="vbat", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + GrowattSensorEntityDescription( + key="mix_pv1_voltage", + name="PV1 voltage", + api_key="vpv1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + GrowattSensorEntityDescription( + key="mix_pv2_voltage", + name="PV2 voltage", + api_key="vpv2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + # Values from 'mix_totals' API call + GrowattSensorEntityDescription( + key="mix_load_consumption_today", + name="Load consumption today", + api_key="elocalLoadToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="elocalLoadTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid_today", + name="Export to grid today", + api_key="etoGridToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid_lifetime", + name="Lifetime export to grid", + api_key="etogridTotal", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + # Values from 'mix_system_status' API call + GrowattSensorEntityDescription( + key="mix_battery_charge", + name="Battery charging", + api_key="chargePower", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption", + name="Load consumption", + api_key="pLocalLoad", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_1", + name="PV1 Wattage", + api_key="pPv1", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_2", + name="PV2 Wattage", + api_key="pPv2", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_wattage_pv_all", + name="All PV Wattage", + api_key="ppv", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_export_to_grid", + name="Export to grid", + api_key="pactogrid", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_import_from_grid", + name="Import from grid", + api_key="pactouser", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_battery_discharge_kw", + name="Battery discharging kW", + api_key="pdisCharge1", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="mix_grid_voltage", + name="Grid voltage", + api_key="vAc1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + # Values from 'mix_detail' API call + GrowattSensorEntityDescription( + key="mix_system_production_today", + name="System production today (self-consumption + export)", + api_key="eCharge", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_solar_today", + name="Load consumption today (solar)", + api_key="eChargeToday", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_self_consumption_today", + name="Self consumption today (solar + battery)", + api_key="eChargeToday1", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_load_consumption_battery_today", + name="Load consumption today (battery)", + api_key="echarge1", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="mix_import_from_grid_today", + name="Import from grid today (load)", + api_key="etouser", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + # This sensor is manually created using the most recent X-Axis value from the chartData + GrowattSensorEntityDescription( + key="mix_last_update", + name="Last Data Update", + api_key="lastdataupdate", + native_unit_of_measurement=None, + device_class=DEVICE_CLASS_TIMESTAMP, + ), + # Values from 'dashboard_data' API call + GrowattSensorEntityDescription( + key="mix_import_from_grid_today_combined", + name="Import from grid today (load + charging)", + api_key="etouser_combined", # This id is not present in the raw API data, it is added by the sensor + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), +) def get_device_list(api, config): @@ -606,42 +711,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) - entities = [] probe = GrowattData(api, username, password, plant_id, "total") - for sensor in TOTAL_SENSOR_TYPES: - entities.append( - GrowattInverter(probe, f"{name} Total", sensor, f"{plant_id}-{sensor}") + entities = [ + GrowattInverter( + probe, + name=f"{name} Total", + unique_id=f"{plant_id}-{description.key}", + description=description, ) + for description in TOTAL_SENSOR_TYPES + ] # Add sensors for each device in the specified plant. for device in devices: probe = GrowattData( api, username, password, device["deviceSn"], device["deviceType"] ) - sensors = [] + sensor_descriptions = () if device["deviceType"] == "inverter": - sensors = INVERTER_SENSOR_TYPES + sensor_descriptions = INVERTER_SENSOR_TYPES elif device["deviceType"] == "storage": probe.plant_id = plant_id - sensors = STORAGE_SENSOR_TYPES + sensor_descriptions = STORAGE_SENSOR_TYPES elif device["deviceType"] == "mix": probe.plant_id = plant_id - sensors = MIX_SENSOR_TYPES + sensor_descriptions = MIX_SENSOR_TYPES else: _LOGGER.debug( "Device type %s was found but is not supported right now", device["deviceType"], ) - for sensor in sensors: - entities.append( + entities.extend( + [ GrowattInverter( probe, - f"{device['deviceAilas']}", - sensor, - f"{device['deviceSn']}-{sensor}", + name=f"{device['deviceAilas']}", + unique_id=f"{device['deviceSn']}-{description.key}", + description=description, ) - ) + for description in sensor_descriptions + ] + ) async_add_entities(entities, True) @@ -649,48 +760,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GrowattInverter(SensorEntity): """Representation of a Growatt Sensor.""" - def __init__(self, probe, name, sensor, unique_id): + entity_description: GrowattSensorEntityDescription + + def __init__( + self, probe, name, unique_id, description: GrowattSensorEntityDescription + ): """Initialize a PVOutput sensor.""" - self.sensor = sensor self.probe = probe - self._name = name - self._state = None - self._unique_id = unique_id + self.entity_description = description + + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = unique_id + self._attr_icon = "mdi:solar-power" @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {SENSOR_TYPES[self.sensor][0]}" - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return self._unique_id - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:solar-power" - - @property - def state(self): + def native_value(self): """Return the state of the sensor.""" - result = self.probe.get_data(SENSOR_TYPES[self.sensor][2]) - round_to = SENSOR_TYPES[self.sensor][3].get("round") - if round_to is not None: - result = round(result, round_to) + result = self.probe.get_data(self.entity_description.api_key) + if self.entity_description.precision is not None: + result = round(result, self.entity_description.precision) return result - @property - def device_class(self): - """Return the device class of the sensor.""" - return SENSOR_TYPES[self.sensor][3].get("device_class") - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self.sensor][1] - def update(self): """Get the latest data from the Growat API and updates the state.""" self.probe.update() diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json index d856d13a96b..5b2efc737fe 100644 --- a/homeassistant/components/growatt_server/translations/hu.json +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -17,6 +17,7 @@ "data": { "name": "N\u00e9v", "password": "Jelsz\u00f3", + "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "title": "Adja meg Growatt adatait" diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json index dee1e989465..8977a7e86a3 100644 --- a/homeassistant/components/growatt_server/translations/no.json +++ b/homeassistant/components/growatt_server/translations/no.json @@ -17,6 +17,7 @@ "data": { "name": "Navn", "password": "Passord", + "url": "URL", "username": "Brukernavn" }, "title": "Skriv inn Growatt-informasjonen din" diff --git a/homeassistant/components/growatt_server/translations/zh-Hans.json b/homeassistant/components/growatt_server/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 812e6a58f28..f8f89b1ea36 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -559,7 +559,7 @@ class GTFSDepartureSensor(SensorEntity): return self._name @property - def state(self) -> str | None: # type: ignore + def native_value(self) -> str | None: # type: ignore """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 2d7cde86cca..ed3cfedba0e 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -126,15 +126,15 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_BATTERY: - self._attr_state = self.coordinator.data["battery"] + self._attr_native_value = self.coordinator.data["battery"] elif self._kind == SENSOR_KIND_TEMPERATURE: - self._attr_state = self.coordinator.data["temperature"] + self._attr_native_value = self.coordinator.data["temperature"] class ValveControllerSensor(ValveControllerEntity, SensorEntity): @@ -153,7 +153,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit async def _async_continue_entity_setup(self) -> None: """Register API interest (and related tasks) when the entity is added.""" @@ -167,11 +167,13 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): self._attr_available = self.coordinators[ API_SYSTEM_ONBOARD_SENSOR_STATUS ].last_update_success - self._attr_state = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ - "temperature" - ] + self._attr_native_value = self.coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].data["temperature"] elif self._kind == SENSOR_KIND_UPTIME: self._attr_available = self.coordinators[ API_SYSTEM_DIAGNOSTICS ].last_update_success - self._attr_state = self.coordinators[API_SYSTEM_DIAGNOSTICS].data["uptime"] + self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ + "uptime" + ] diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index ca9a746f9d9..15469bead1e 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -13,7 +13,11 @@ "data": { "ip_address": "IP c\u00edm", "port": "Port" - } + }, + "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." + }, + "zeroconf_confirm": { + "description": "Be akarja \u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" } } } diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index efb82a9f1aa..1d1536d1679 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ARGS, @@ -83,7 +84,7 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Habitica service.""" configs = config.get(DOMAIN, []) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 52748ddadad..eb42426e8ea 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -155,12 +155,12 @@ class HabitipySensor(SensorEntity): return f"{DOMAIN}_{self._name}_{self._sensor_name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._sensor_type.unit @@ -195,7 +195,7 @@ class HabitipyTaskSensor(SensorEntity): return f"{DOMAIN}_{self._name}_{self._task_name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -220,6 +220,6 @@ class HabitipyTaskSensor(SensorEntity): return attrs @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._task_type.unit diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index 3c065b01169..2f02ba9f623 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -19,6 +19,7 @@ }, "user": { "data": { + "authorization_code": "Enged\u00e9lyez\u00e9si k\u00f3d (k\u00e9zi hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges)", "email": "E-mail", "password": "Jelsz\u00f3" }, diff --git a/homeassistant/components/hangouts/translations/lt.json b/homeassistant/components/hangouts/translations/lt.json new file mode 100644 index 00000000000..13dbbf8bdbc --- /dev/null +++ b/homeassistant/components/hangouts/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "2FA PIN" + }, + "title": "2 veiksni\u0173 autentifikavimas" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index a9cb6ccecee..4922bbd1ac6 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -7,11 +7,29 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name}", "step": { + "link": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + }, "user": { "data": { - "host": "Hoszt" - } + "host": "Hoszt", + "name": "Hub neve" + }, + "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Az alap\u00e9rtelmezett tev\u00e9kenys\u00e9g, amelyet akkor kell v\u00e9grehajtani, ha nincs megadva.", + "delay_secs": "A parancsok k\u00fcld\u00e9se k\u00f6z\u00f6tti k\u00e9s\u00e9s." + }, + "description": "Harmony Hub be\u00e1ll\u00edt\u00e1sok" } } } diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 01930b5ec0e..dfd13adbde6 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,15 +1,28 @@ """Binary sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_UPDATE_AVAILABLE +from .const import ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS from .entity import HassioAddonEntity, HassioOSEntity +ENTITY_DESCRIPTIONS = ( + BinarySensorEntityDescription( + device_class=DEVICE_CLASS_UPDATE, + entity_registry_enabled_default=False, + key=ATTR_UPDATE_AVAILABLE, + name="Update Available", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -19,16 +32,26 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [ - HassioAddonBinarySensor( - coordinator, addon, ATTR_UPDATE_AVAILABLE, "Update Available" - ) - for addon in coordinator.data["addons"].values() - ] - if coordinator.is_hass_os: - entities.append( - HassioOSBinarySensor(coordinator, ATTR_UPDATE_AVAILABLE, "Update Available") - ) + entities = [] + + for entity_description in ENTITY_DESCRIPTIONS: + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + entities.append( + HassioAddonBinarySensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) + ) + + if coordinator.is_hass_os: + entities.append( + HassioOSBinarySensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) + async_add_entities(entities) @@ -38,7 +61,9 @@ class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.addon_info[self.attribute_name] + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + self.entity_description.key + ] class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): @@ -47,4 +72,4 @@ class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.os_info[self.attribute_name] + return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 6104e57fb17..134fba15f70 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -40,7 +40,6 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" -# Add-on keys ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" @@ -49,6 +48,10 @@ ATTR_URL = "url" ATTR_REPOSITORY = "repository" +DATA_KEY_ADDONS = "addons" +DATA_KEY_OS = "os" + + class SupervisorEntityModel(str, Enum): """Supervisor entity model.""" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 4885ba8979f..4a342e9965f 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.const import ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator @@ -17,42 +17,16 @@ class HassioAddonEntity(CoordinatorEntity): def __init__( self, coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, addon: dict[str, Any], - attribute_name: str, - sensor_name: str, ) -> None: """Initialize base entity.""" - self.addon_slug = addon[ATTR_SLUG] - self.addon_name = addon[ATTR_NAME] - self._data_key = "addons" - self.attribute_name = attribute_name - self.sensor_name = sensor_name super().__init__(coordinator) - - @property - def addon_info(self) -> dict[str, Any]: - """Return add-on info.""" - return self.coordinator.data[self._data_key][self.addon_slug] - - @property - def name(self) -> str: - """Return entity name.""" - return f"{self.addon_name}: {self.sensor_name}" - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def unique_id(self) -> str: - """Return unique ID for entity.""" - return f"{self.addon_slug}_{self.attribute_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return {"identifiers": {(DOMAIN, self.addon_slug)}} + self.entity_description = entity_description + self._addon_slug = addon[ATTR_SLUG] + self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" + self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, addon[ATTR_SLUG])}} class HassioOSEntity(CoordinatorEntity): @@ -61,36 +35,11 @@ class HassioOSEntity(CoordinatorEntity): def __init__( self, coordinator: HassioDataUpdateCoordinator, - attribute_name: str, - sensor_name: str, + entity_description: EntityDescription, ) -> None: """Initialize base entity.""" - self._data_key = "os" - self.attribute_name = attribute_name - self.sensor_name = sensor_name super().__init__(coordinator) - - @property - def os_info(self) -> dict[str, Any]: - """Return OS info.""" - return self.coordinator.data[self._data_key] - - @property - def name(self) -> str: - """Return entity name.""" - return f"Home Assistant Operating System: {self.sensor_name}" - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def unique_id(self) -> str: - """Return unique ID for entity.""" - return f"home_assistant_os_{self.attribute_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return {"identifiers": {(DOMAIN, "OS")}} + self.entity_description = entity_description + self._attr_name = f"Home Assistant Operating System: {entity_description.name}" + self._attr_unique_id = f"home_assistant_os_{entity_description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, "OS")}} diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index e81980d78e1..55678eb29c4 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,15 +1,28 @@ """Sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_VERSION, ATTR_VERSION_LATEST +from .const import ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, DATA_KEY_OS from .entity import HassioAddonEntity, HassioOSEntity +ENTITY_DESCRIPTIONS = ( + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_VERSION, + name="Version", + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_VERSION_LATEST, + name="Newest Version", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -21,16 +34,23 @@ async def async_setup_entry( entities = [] - for attribute_name, sensor_name in ( - (ATTR_VERSION, "Version"), - (ATTR_VERSION_LATEST, "Newest Version"), - ): - for addon in coordinator.data["addons"].values(): + for entity_description in ENTITY_DESCRIPTIONS: + for addon in coordinator.data[DATA_KEY_ADDONS].values(): entities.append( - HassioAddonSensor(coordinator, addon, attribute_name, sensor_name) + HassioAddonSensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) ) + if coordinator.is_hass_os: - entities.append(HassioOSSensor(coordinator, attribute_name, sensor_name)) + entities.append( + HassioOSSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) async_add_entities(entities) @@ -39,15 +59,17 @@ class HassioAddonSensor(HassioAddonEntity, SensorEntity): """Sensor to track a Hass.io add-on attribute.""" @property - def state(self) -> str: - """Return state of entity.""" - return self.addon_info[self.attribute_name] + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + self.entity_description.key + ] class HassioOSSensor(HassioOSEntity, SensorEntity): """Sensor to track a Hass.io add-on attribute.""" @property - def state(self) -> str: - """Return state of entity.""" - return self.os_info[self.attribute_name] + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 55b369c2fde..738837989b9 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -69,12 +69,12 @@ class HaveIBeenPwnedSensor(SensorEntity): return f"Breaches {self._email}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 8169fa811e0..49d1c2f28fa 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -78,7 +78,7 @@ class HddTempSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -88,7 +88,7 @@ class HddTempSensor(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 9d4fa286fd6..87391634251 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -45,6 +45,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType DOMAIN = "hdmi_cec" @@ -186,7 +187,7 @@ def parse_mapping(mapping, parents=None): yield (val, pad_physical_address(cur)) -def setup(hass: HomeAssistant, base_config): # noqa: C901 +def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 """Set up the CEC capability.""" # Parse configuration into a dict of device name to physical address diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 7490c1e5be1..35520927e97 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -43,7 +43,7 @@ MIN_UPDATE_SOURCES = timedelta(seconds=1) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HEOS component.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 2fbce1993cd..c487b49ee47 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -10,7 +10,9 @@ "user": { "data": { "host": "Hoszt" - } + }, + "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", + "title": "Csatlakoz\u00e1s a Heos-hoz" } } } diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 11fd19bd895..7606a2772d6 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -256,7 +256,7 @@ class HERETravelTimeSensor(SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if self._here_data.traffic_mode and self._here_data.traffic_time is not None: return str(round(self._here_data.traffic_time / 60)) @@ -292,7 +292,7 @@ class HERETravelTimeSensor(SensorEntity): return res @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 3651dd8295f..518e555c280 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -167,7 +167,6 @@ async def ws_get_statistics_during_period( vol.Optional("statistic_type"): vol.Any("sum", "mean"), } ) -@websocket_api.require_admin @websocket_api.async_response async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict @@ -393,7 +392,7 @@ class Filters: if includes and not excludes: return or_(*includes) - if not excludes and includes: + if not includes and excludes: return not_(or_(*excludes)) return or_(*includes) & not_(or_(*excludes)) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index e8ff9afc4e3..0db311b0354 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -153,7 +153,7 @@ class HistoryStatsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.value is None or self.count is None: return None @@ -168,7 +168,7 @@ class HistoryStatsSensor(SensorEntity): return self.count @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index f21afc51801..5ea81bff123 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -57,7 +57,7 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return DEVICETYPE[self.device["hiveType"]].get("type") @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEVICETYPE[self.device["hiveType"]].get("unit") @@ -67,7 +67,7 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return self.device["haName"] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.device["status"]["state"] diff --git a/homeassistant/components/hive/translations/zh-Hans.json b/homeassistant/components/hive/translations/zh-Hans.json new file mode 100644 index 00000000000..780a47cb958 --- /dev/null +++ b/homeassistant/components/hive/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_password": "\u65e0\u6cd5\u767b\u5f55 Hive\uff0c\u5bc6\u7801\u9519\u8bef\uff0c\u8bf7\u91cd\u8bd5\u3002" + }, + "step": { + "reauth": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index f8a9157dca2..1fc446af401 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -34,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 463de6cda51..373ad6be295 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -42,7 +42,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): self._sign = sign @property - def state(self): + def native_value(self): """Return true if the binary sensor is on.""" return self._state @@ -83,7 +83,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): _LOGGER.debug("Updated, new state: %s", self._state) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e775b9d97aa..ffb055e6324 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import ( dispatcher, ) from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow, helpers @@ -50,7 +51,7 @@ PLATFORMS = ["switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Legrand Home+ Control component from configuration.yaml.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index e798fda209b..2314d2b0c1b 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -14,17 +14,19 @@ from homeassistant.const import ( RESTART_EXIT_CODE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, + SERVICE_SAVE_PERSISTENT_STATES, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser -from homeassistant.helpers import config_validation as cv, recorder +from homeassistant.helpers import config_validation as cv, recorder, restore_state from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, ) +from homeassistant.helpers.typing import ConfigType ATTR_ENTRY_ID = "entry_id" @@ -50,9 +52,13 @@ SCHEMA_RELOAD_CONFIG_ENTRY = vol.All( SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) -async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C901 +async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" + async def async_save_persistent_states(service): + """Handle calls to homeassistant.save_persistent_states.""" + await restore_state.RestoreStateData.async_save_persistent_states(hass) + async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" referenced = await async_extract_referenced_entity_ids(hass, service) @@ -114,6 +120,10 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C9 if tasks: await asyncio.gather(*tasks) + hass.services.async_register( + ha.DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states + ) + service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA) hass.services.async_register( diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 251ee171b6a..da52ff50d2f 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -74,3 +74,9 @@ reload_config_entry: example: 8955375327824e14ba89e4b29cc3ec9a selector: text: + +save_persistent_states: + name: Save Persistent States + description: + Save the persistent states (for entities derived from RestoreEntity) immediately. + Maintain the normal periodic saving interval. diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 562a7335617..0a9342afa69 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -10,6 +10,7 @@ "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", "timezone": "Zona horaria", + "user": "Usuario", "version": "Versi\u00f3n", "virtualenv": "Entorno virtual" } diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json index f86d7b0dca0..20de5a2d1b7 100644 --- a/homeassistant/components/homeassistant/translations/he.json +++ b/homeassistant/components/homeassistant/translations/he.json @@ -8,6 +8,7 @@ "os_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4", "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df", "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df", + "user": "\u05de\u05e9\u05ea\u05de\u05e9", "version": "\u05d2\u05d9\u05e8\u05e1\u05d4" } } diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 9eddeeba112..b4da84596bf 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -10,6 +10,7 @@ "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja", "python_version": "Python verzi\u00f3", "timezone": "Id\u0151z\u00f3na", + "user": "Felhaszn\u00e1l\u00f3", "version": "Verzi\u00f3", "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet" } diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 325bb53db15..675c02a6b66 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -10,6 +10,7 @@ "os_version": "Operativsystemversjon", "python_version": "Python versjon", "timezone": "Tidssone", + "user": "Bruker", "version": "Versjon", "virtualenv": "Virtuelt milj\u00f8" } diff --git a/homeassistant/components/homeassistant/translations/zh-Hans.json b/homeassistant/components/homeassistant/translations/zh-Hans.json index 617866926b8..e640d502e0c 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hans.json +++ b/homeassistant/components/homeassistant/translations/zh-Hans.json @@ -10,6 +10,7 @@ "os_version": "\u64cd\u4f5c\u7cfb\u7edf\u7248\u672c", "python_version": "Python \u7248\u672c", "timezone": "\u65f6\u533a", + "user": "\u7528\u6237", "version": "\u7248\u672c", "virtualenv": "\u865a\u62df\u73af\u5883" } diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 967acaf7ddc..705b671f28a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -44,6 +44,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_extract_referenced_entity_ids +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, async_get_integration from . import ( # noqa: F401 @@ -187,7 +188,7 @@ def _async_get_entries_by_name(current_entries): return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomeKit from yaml.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c1f5078e2d6..ec6ef670f44 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" @@ -238,9 +243,10 @@ class HomeAccessory(Accessory): model = self.config[ATTR_MODEL] else: model = domain.title() + sw_version = None if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_sw_version(self.config[ATTR_SW_VERSION]) - else: + if sw_version is None: sw_version = __version__ self.set_info_service( diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 187d730de2f..c63ce2a8927 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -6,8 +6,7 @@ "HAP-python==4.0.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", - "base36==0.1.1", - "PyTurboJPEG==1.5.0" + "base36==0.1.1" ], "dependencies": ["http", "camera", "ffmpeg", "network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index 1afc0183a0d..c6fdf0afd74 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "port_name_in_use": "Az azonos nev\u0171 vagy port\u00fa tartoz\u00e9k vagy h\u00edd m\u00e1r konfigur\u00e1lva van." + }, "step": { "pairing": { + "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d szakasz \u201e\u00c9rtes\u00edt\u00e9sek\u201d szakasz\u00e1ban tal\u00e1lhat\u00f3 utas\u00edt\u00e1sokat.", "title": "HomeKit p\u00e1ros\u00edt\u00e1s" }, "user": { "data": { "include_domains": "Felvenni k\u00edv\u00e1nt domainek" }, + "description": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt domaineket. A domain minden t\u00e1mogatott entit\u00e1sa szerepelni fog. Minden tartoz\u00e9k m\u00f3dban k\u00fcl\u00f6n HomeKit p\u00e9ld\u00e1ny j\u00f6n l\u00e9tre minden TV m\u00e9dialej\u00e1tsz\u00f3hoz, tev\u00e9kenys\u00e9g alap\u00fa t\u00e1vir\u00e1ny\u00edt\u00f3hoz, z\u00e1rhoz \u00e9s f\u00e9nyk\u00e9pez\u0151g\u00e9phez.", "title": "Felvenni k\u00edv\u00e1nt domainek kiv\u00e1laszt\u00e1sa" } } @@ -15,12 +20,17 @@ "options": { "step": { "advanced": { + "data": { + "auto_start": "Automatikus ind\u00edt\u00e1s (tiltsa le, ha manu\u00e1lisan h\u00edvja a homekit.start szolg\u00e1ltat\u00e1st)" + }, + "description": "Ezeket a be\u00e1ll\u00edt\u00e1sokat csak akkor kell m\u00f3dos\u00edtani, ha a HomeKit nem m\u0171k\u00f6dik.", "title": "Halad\u00f3 be\u00e1ll\u00edt\u00e1sok" }, "cameras": { "data": { "camera_copy": "A nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k" }, + "description": "Ellen\u0151rizze az \u00f6sszes kamer\u00e1t, amely t\u00e1mogatja a nat\u00edv H.264 adatfolyamokat. Ha a f\u00e9nyk\u00e9pez\u0151g\u00e9p nem ad ki H.264 adatfolyamot, a rendszer \u00e1tk\u00f3dolja a vide\u00f3t H.264 form\u00e1tumba a HomeKit sz\u00e1m\u00e1ra. Az \u00e1tk\u00f3dol\u00e1shoz nagy teljes\u00edtm\u00e9ny\u0171 CPU sz\u00fcks\u00e9ges, \u00e9s val\u00f3sz\u00edn\u0171leg nem fog m\u0171k\u00f6dni egylapos sz\u00e1m\u00edt\u00f3g\u00e9peken.", "title": "V\u00e1laszd ki a kamera vide\u00f3 kodekj\u00e9t." }, "include_exclude": { @@ -28,6 +38,7 @@ "entities": "Entit\u00e1sok", "mode": "M\u00f3d" }, + "description": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s szerepel. H\u00eddbefogad\u00e1si m\u00f3dban a tartom\u00e1ny \u00f6sszes entit\u00e1sa szerepelni fog, hacsak nincsenek kijel\u00f6lve konkr\u00e9t entit\u00e1sok. H\u00eddkiz\u00e1r\u00e1si m\u00f3dban a domain \u00f6sszes entit\u00e1sa szerepelni fog, kiv\u00e9ve a kiz\u00e1rt entit\u00e1sokat. A legjobb teljes\u00edtm\u00e9ny \u00e9rdek\u00e9ben minden TV m\u00e9dialej\u00e1tsz\u00f3hoz, tev\u00e9kenys\u00e9galap\u00fa t\u00e1vir\u00e1ny\u00edt\u00f3hoz, z\u00e1rhoz \u00e9s f\u00e9nyk\u00e9pez\u0151g\u00e9phez k\u00fcl\u00f6n HomeKit tartoz\u00e9kot hoznak l\u00e9tre.", "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" }, "init": { @@ -35,6 +46,7 @@ "include_domains": "Felvenni k\u00edv\u00e1nt domainek", "mode": "M\u00f3d" }, + "description": "A HomeKit konfigur\u00e1lhat\u00f3 \u00fagy, hogy egy h\u00edd vagy egyetlen tartoz\u00e9k l\u00e1that\u00f3 legyen. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s haszn\u00e1lhat\u00f3. A tartoz\u00e9k m\u00f3dra van sz\u00fcks\u00e9g ahhoz, hogy a TV -eszk\u00f6zoszt\u00e1ly\u00fa m\u00e9dialej\u00e1tsz\u00f3k megfelel\u0151en m\u0171k\u00f6djenek. A \u201eTartalmazand\u00f3 tartom\u00e1nyok\u201d entit\u00e1sai szerepelni fognak a HomeKitben. A k\u00f6vetkez\u0151 k\u00e9perny\u0151n kiv\u00e1laszthatja, hogy mely entit\u00e1sokat k\u00edv\u00e1nja felvenni vagy kiz\u00e1rni a list\u00e1b\u00f3l.", "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt domaineket." }, "yaml": { diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 7de5494c56a..2a4f1497e2f 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domener \u00e5 inkludere" }, - "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert. Det opprettes en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", + "description": "Velg domenene som skal inkluderes. Alle enheter som st\u00f8ttes p\u00e5 domenet vil bli inkludert. En egen HomeKit -forekomst i tilbeh\u00f8rsmodus vil bli opprettet for hver tv -mediespiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", "title": "Velg domener som skal inkluderes" } } diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 077366870e2..4a8999ede08 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -55,7 +55,6 @@ from .const import ( SERV_SPEAKER, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) -from .img_util import scale_jpeg_camera_image from .util import pid_is_alive _LOGGER = logging.getLogger(__name__) @@ -467,8 +466,9 @@ class Camera(HomeAccessory, PyhapCamera): async def async_get_snapshot(self, image_size): """Return a jpeg of a snapshot from the camera.""" - return scale_jpeg_camera_image( - await self.hass.components.camera.async_get_image(self.entity_id), - image_size["image-width"], - image_size["image-height"], + image = await self.hass.components.camera.async_get_image( + self.entity_id, + width=image_size["image-width"], + height=image_size["image-height"], ) + return image.content diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 099eced62d3..4c501208ca5 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -178,18 +178,11 @@ class GarageDoorOpener(HomeAccessory): obstruction_detected = ( new_state.attributes[ATTR_OBSTRUCTION_DETECTED] is True ) - if self.char_obstruction_detected.value != obstruction_detected: - self.char_obstruction_detected.set_value(obstruction_detected) + self.char_obstruction_detected.set_value(obstruction_detected) - if ( - target_door_state is not None - and self.char_target_state.value != target_door_state - ): + if target_door_state is not None: self.char_target_state.set_value(target_door_state) - if ( - current_door_state is not None - and self.char_current_state.value != current_door_state - ): + if current_door_state is not None: self.char_current_state.set_value(current_door_state) @@ -260,10 +253,8 @@ class OpeningDeviceBase(HomeAccessory): # We'll have to normalize to [0,100] current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 current_tilt = int(current_tilt) - if self.char_current_tilt.value != current_tilt: - self.char_current_tilt.set_value(current_tilt) - if self.char_target_tilt.value != current_tilt: - self.char_target_tilt.set_value(current_tilt) + self.char_current_tilt.set_value(current_tilt) + self.char_target_tilt.set_value(current_tilt) class OpeningDevice(OpeningDeviceBase, HomeAccessory): @@ -312,14 +303,11 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): current_position = int(current_position) - if self.char_current_position.value != current_position: - self.char_current_position.set_value(current_position) - if self.char_target_position.value != current_position: - self.char_target_position.set_value(current_position) + self.char_current_position.set_value(current_position) + self.char_target_position.set_value(current_position) position_state = _hass_state_to_position_start(new_state.state) - if self.char_position_state.value != position_state: - self.char_position_state.set_value(position_state) + self.char_position_state.set_value(position_state) super().async_update_state(new_state) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 1a0bb41774c..85157dd9367 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -193,16 +193,14 @@ class Fan(HomeAccessory): state = new_state.state if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 - if self.char_active.value != self._state: - self.char_active.set_value(self._state) + self.char_active.set_value(self._state) # Handle Direction if self.char_direction is not None: direction = new_state.attributes.get(ATTR_DIRECTION) if direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): hk_direction = 1 if direction == DIRECTION_REVERSE else 0 - if self.char_direction.value != hk_direction: - self.char_direction.set_value(hk_direction) + self.char_direction.set_value(hk_direction) # Handle Speed if self.char_speed is not None and state != STATE_OFF: @@ -222,7 +220,7 @@ class Fan(HomeAccessory): # in order to avoid this incorrect behavior. if percentage == 0 and state == STATE_ON: percentage = 1 - if percentage is not None and self.char_speed.value != percentage: + if percentage is not None: self.char_speed.set_value(percentage) # Handle Oscillating @@ -230,11 +228,9 @@ class Fan(HomeAccessory): oscillating = new_state.attributes.get(ATTR_OSCILLATING) if isinstance(oscillating, bool): hk_oscillating = 1 if oscillating else 0 - if self.char_swing.value != hk_oscillating: - self.char_swing.set_value(hk_oscillating) + self.char_swing.set_value(hk_oscillating) current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) for preset_mode, char in self.preset_mode_chars.items(): hk_value = 1 if preset_mode == current_preset_mode else 0 - if char.value != hk_value: - char.set_value(hk_value) + char.set_value(hk_value) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index a4a73abf998..6371f883b09 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -224,8 +224,7 @@ class HumidifierDehumidifier(HomeAccessory): is_active = new_state.state == STATE_ON # Update active state - if self.char_active.value != is_active: - self.char_active.set_value(is_active) + self.char_active.set_value(is_active) # Set current state if is_active: @@ -235,13 +234,9 @@ class HumidifierDehumidifier(HomeAccessory): current_state = HC_STATE_DEHUMIDIFYING else: current_state = HC_STATE_INACTIVE - if self.char_current_humidifier_dehumidifier.value != current_state: - self.char_current_humidifier_dehumidifier.set_value(current_state) + self.char_current_humidifier_dehumidifier.set_value(current_state) # Update target humidity target_humidity = new_state.attributes.get(ATTR_HUMIDITY) - if ( - isinstance(target_humidity, (int, float)) - and self.char_target_humidity.value != target_humidity - ): + if isinstance(target_humidity, (int, float)): self.char_target_humidity.set_value(target_humidity) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 88e21272a4f..90c55d52153 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,18 +1,17 @@ """Class to hold all light accessories.""" import logging +import math from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, DOMAIN, brightness_supported, color_supported, @@ -25,13 +24,17 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, + color_temperature_to_hs, +) from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, - CHAR_NAME, CHAR_ON, CHAR_SATURATION, PROP_MAX_VALUE, @@ -43,6 +46,8 @@ _LOGGER = logging.getLogger(__name__) RGB_COLOR = "rgb_color" +CHANGE_COALESCE_TIME_WINDOW = 0.01 + @TYPES.register("Light") class Light(HomeAccessory): @@ -55,102 +60,78 @@ class Light(HomeAccessory): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self.chars_primary = [] - self.chars_secondary = [] + self.chars = [] + self._event_timer = None + self._pending_events = {} state = self.hass.states.get(self.entity_id) attributes = state.attributes color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES) - self.is_color_supported = color_supported(color_modes) - self.is_color_temp_supported = color_temp_supported(color_modes) - self.color_and_temp_supported = ( - self.is_color_supported and self.is_color_temp_supported - ) - self.is_brightness_supported = brightness_supported(color_modes) + self.color_supported = color_supported(color_modes) + self.color_temp_supported = color_temp_supported(color_modes) + self.brightness_supported = brightness_supported(color_modes) - if self.is_brightness_supported: - self.chars_primary.append(CHAR_BRIGHTNESS) + if self.brightness_supported: + self.chars.append(CHAR_BRIGHTNESS) - if self.is_color_supported: - self.chars_primary.append(CHAR_HUE) - self.chars_primary.append(CHAR_SATURATION) + if self.color_supported: + self.chars.extend([CHAR_HUE, CHAR_SATURATION]) - if self.is_color_temp_supported: - if self.color_and_temp_supported: - self.chars_primary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_COLOR_TEMPERATURE) - if self.is_brightness_supported: - self.chars_secondary.append(CHAR_BRIGHTNESS) - else: - self.chars_primary.append(CHAR_COLOR_TEMPERATURE) + if self.color_temp_supported: + self.chars.append(CHAR_COLOR_TEMPERATURE) - serv_light_primary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_primary - ) - serv_light_secondary = None - self.char_on_primary = serv_light_primary.configure_char(CHAR_ON, value=0) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char(CHAR_ON, value=0) - if self.color_and_temp_supported: - serv_light_secondary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_secondary - ) - serv_light_primary.add_linked_service(serv_light_secondary) - serv_light_primary.configure_char(CHAR_NAME, value="RGB") - self.char_on_secondary = serv_light_secondary.configure_char( - CHAR_ON, value=0 - ) - serv_light_secondary.configure_char(CHAR_NAME, value="Temperature") - - if self.is_brightness_supported: + if self.brightness_supported: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. - self.char_brightness_primary = serv_light_primary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) - if self.chars_secondary: - self.char_brightness_secondary = serv_light_secondary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) + self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) - if self.is_color_temp_supported: - min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) - max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) - serv_light = serv_light_secondary or serv_light_primary - self.char_color_temperature = serv_light.configure_char( + if self.color_temp_supported: + min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) + max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) + self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, ) - if self.is_color_supported: - self.char_hue = serv_light_primary.configure_char(CHAR_HUE, value=0) - self.char_saturation = serv_light_primary.configure_char( - CHAR_SATURATION, value=75 - ) + if self.color_supported: + self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) + self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.async_update_state(state) + serv_light.setter_callback = self._set_chars - if self.color_and_temp_supported: - serv_light_primary.setter_callback = self._set_chars_primary - serv_light_secondary.setter_callback = self._set_chars_secondary - else: - serv_light_primary.setter_callback = self._set_chars + def _set_chars(self, char_values): + _LOGGER.debug("Light _set_chars: %s", char_values) + # Newest change always wins + if CHAR_COLOR_TEMPERATURE in self._pending_events and ( + CHAR_SATURATION in char_values or CHAR_HUE in char_values + ): + del self._pending_events[CHAR_COLOR_TEMPERATURE] + for char in (CHAR_HUE, CHAR_SATURATION): + if char in self._pending_events and CHAR_COLOR_TEMPERATURE in char_values: + del self._pending_events[char] - def _set_chars_primary(self, char_values): - """Primary service is RGB or W if only color or color temp is supported.""" - self._set_chars(char_values, True) + self._pending_events.update(char_values) + if self._event_timer: + self._event_timer() + self._event_timer = async_call_later( + self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events + ) - def _set_chars_secondary(self, char_values): - """Secondary service is W if both color or color temp are supported.""" - self._set_chars(char_values, False) - - def _set_chars(self, char_values, is_primary=None): - _LOGGER.debug("Light _set_chars: %s, is_primary: %s", char_values, is_primary) + def _send_events(self, *_): + """Process all changes at once.""" + _LOGGER.debug("Coalesced _set_chars: %s", self._pending_events) + char_values = self._pending_events + self._pending_events = {} events = [] service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} + if CHAR_ON in char_values: if not char_values[CHAR_ON]: service = SERVICE_TURN_OFF @@ -170,24 +151,16 @@ class Light(HomeAccessory): ) return - if self.is_color_temp_supported and ( - is_primary is False or CHAR_COLOR_TEMPERATURE in char_values - ): - params[ATTR_COLOR_TEMP] = char_values.get( - CHAR_COLOR_TEMPERATURE, self.char_color_temperature.value - ) + if CHAR_COLOR_TEMPERATURE in char_values: + params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") - if self.is_color_supported and ( - is_primary is True - or (CHAR_HUE in char_values and CHAR_SATURATION in char_values) - ): - color = ( + elif CHAR_HUE in char_values or CHAR_SATURATION in char_values: + color = params[ATTR_HS_COLOR] = ( char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_SATURATION, self.char_saturation.value), ) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) - params[ATTR_HS_COLOR] = color events.append(f"set color at {color}") self.async_call_service(DOMAIN, service, params, ", ".join(events)) @@ -198,23 +171,10 @@ class Light(HomeAccessory): # Handle State state = new_state.state attributes = new_state.attributes - char_on_value = int(state == STATE_ON) - - if self.color_and_temp_supported: - color_mode = attributes.get(ATTR_COLOR_MODE) - color_temp_mode = color_mode == COLOR_MODE_COLOR_TEMP - primary_on_value = char_on_value if not color_temp_mode else 0 - secondary_on_value = char_on_value if color_temp_mode else 0 - if self.char_on_primary.value != primary_on_value: - self.char_on_primary.set_value(primary_on_value) - if self.char_on_secondary.value != secondary_on_value: - self.char_on_secondary.set_value(secondary_on_value) - else: - if self.char_on_primary.value != char_on_value: - self.char_on_primary.set_value(char_on_value) + self.char_on.set_value(int(state == STATE_ON)) # Handle Brightness - if self.is_brightness_supported: + if self.brightness_supported: brightness = attributes.get(ATTR_BRIGHTNESS) if isinstance(brightness, (int, float)): brightness = round(brightness / 255 * 100, 0) @@ -230,29 +190,25 @@ class Light(HomeAccessory): # order to avoid this incorrect behavior. if brightness == 0 and state == STATE_ON: brightness = 1 - if self.char_brightness_primary.value != brightness: - self.char_brightness_primary.set_value(brightness) - if ( - self.color_and_temp_supported - and self.char_brightness_secondary.value != brightness - ): - self.char_brightness_secondary.set_value(brightness) + self.char_brightness.set_value(brightness) + + # Handle Color - color must always be set before color temperature + # or the iOS UI will not display it correctly. + if self.color_supported: + if ATTR_COLOR_TEMP in attributes: + hue, saturation = color_temperature_to_hs( + color_temperature_mired_to_kelvin( + new_state.attributes[ATTR_COLOR_TEMP] + ) + ) + else: + hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) + if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): + self.char_hue.set_value(round(hue, 0)) + self.char_saturation.set_value(round(saturation, 0)) # Handle color temperature - if self.is_color_temp_supported: - color_temperature = attributes.get(ATTR_COLOR_TEMP) - if isinstance(color_temperature, (int, float)): - color_temperature = round(color_temperature, 0) - if self.char_color_temperature.value != color_temperature: - self.char_color_temperature.set_value(color_temperature) - - # Handle Color - if self.is_color_supported: - hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) - if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): - hue = round(hue, 0) - saturation = round(saturation, 0) - if hue != self.char_hue.value: - self.char_hue.set_value(hue) - if saturation != self.char_saturation.value: - self.char_saturation.set_value(saturation) + if self.color_temp_supported: + color_temp = attributes.get(ATTR_COLOR_TEMP) + if isinstance(color_temp, (int, float)): + self.char_color_temp.set_value(round(color_temp, 0)) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 3a10a0a2f5a..af7501e1869 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -106,14 +106,10 @@ class Lock(HomeAccessory): # LockTargetState only supports locked and unlocked # Must set lock target state before current state # or there will be no notification - if ( - target_lock_state is not None - and self.char_target_state.value != target_lock_state - ): + if target_lock_state is not None: self.char_target_state.set_value(target_lock_state) # Set lock current state ONLY after ensuring that # target state is correct or there will be no # notification - if self.char_current_state.value != current_lock_state: - self.char_current_state.set_value(current_lock_state) + self.char_current_state.set_value(current_lock_state) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 081053d2591..7be1b98dcdb 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -180,8 +180,7 @@ class MediaPlayer(HomeAccessory): _LOGGER.debug( '%s: Set current state for "on_off" to %s', self.entity_id, hk_state ) - if self.chars[FEATURE_ON_OFF].value != hk_state: - self.chars[FEATURE_ON_OFF].set_value(hk_state) + self.chars[FEATURE_ON_OFF].set_value(hk_state) if self.chars[FEATURE_PLAY_PAUSE]: hk_state = current_state == STATE_PLAYING @@ -190,8 +189,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, hk_state, ) - if self.chars[FEATURE_PLAY_PAUSE].value != hk_state: - self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) + self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING @@ -200,8 +198,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, hk_state, ) - if self.chars[FEATURE_PLAY_STOP].value != hk_state: - self.chars[FEATURE_PLAY_STOP].set_value(hk_state) + self.chars[FEATURE_PLAY_STOP].set_value(hk_state) if self.chars[FEATURE_TOGGLE_MUTE]: current_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) @@ -210,8 +207,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, current_state, ) - if self.chars[FEATURE_TOGGLE_MUTE].value != current_state: - self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) + self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) @TYPES.register("TelevisionMediaPlayer") @@ -341,8 +337,7 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): if current_state not in MEDIA_PLAYER_OFF_STATES: hk_state = 1 _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) - if self.char_active.value != hk_state: - self.char_active.set_value(hk_state) + self.char_active.set_value(hk_state) # Set mute state if CHAR_VOLUME_SELECTOR in self.chars_speaker: @@ -352,7 +347,6 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): self.entity_id, current_mute_state, ) - if self.char_mute.value != current_mute_state: - self.char_mute.set_value(current_mute_state) + self.char_mute.set_value(current_mute_state) self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 9e54221430c..53659adef77 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -154,8 +154,7 @@ class RemoteInputSelectAccessory(HomeAccessory): _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) if source_name in self.sources: index = self.sources.index(source_name) - if self.char_input_source.value != index: - self.char_input_source.set_value(index) + self.char_input_source.set_value(index) return possible_sources = new_state.attributes.get(self.source_list_key, []) @@ -174,8 +173,7 @@ class RemoteInputSelectAccessory(HomeAccessory): source_name, possible_sources, ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + self.char_input_source.set_value(0) @TYPES.register("ActivityRemote") @@ -225,7 +223,6 @@ class ActivityRemote(RemoteInputSelectAccessory): # Power state remote hk_state = 1 if current_state == STATE_ON else 0 _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) - if self.char_active.value != hk_state: - self.char_active.set_value(hk_state) + self.char_active.set_value(hk_state) self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 6fe1a4e9e29..d76fbf0f534 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -158,15 +158,12 @@ class SecuritySystem(HomeAccessory): """Update security state after state changed.""" hass_state = new_state.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_state, - ) - + self.char_current_state.set_value(current_state) + _LOGGER.debug( + "%s: Updated current state to %s (%d)", + self.entity_id, + hass_state, + current_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) + self.char_target_state.set_value(target_state) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index b6cc4b05125..bcef7564fa3 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -101,11 +101,10 @@ class TemperatureSensor(HomeAccessory): temperature = convert_to_float(new_state.state) if temperature: temperature = temperature_to_homekit(temperature, unit) - if self.char_temp.value != temperature: - self.char_temp.set_value(temperature) - _LOGGER.debug( - "%s: Current temperature set to %.1f°C", self.entity_id, temperature - ) + self.char_temp.set_value(temperature) + _LOGGER.debug( + "%s: Current temperature set to %.1f°C", self.entity_id, temperature + ) @TYPES.register("HumiditySensor") @@ -128,7 +127,7 @@ class HumiditySensor(HomeAccessory): def async_update_state(self, new_state): """Update accessory after state change.""" humidity = convert_to_float(new_state.state) - if humidity and self.char_humidity.value != humidity: + if humidity: self.char_humidity.set_value(humidity) _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) @@ -161,9 +160,8 @@ class AirQualitySensor(HomeAccessory): self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) air_quality = density_to_air_quality(density) - if self.char_quality.value != air_quality: - self.char_quality.set_value(air_quality) - _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) @TYPES.register("CarbonMonoxideSensor") @@ -194,14 +192,12 @@ class CarbonMonoxideSensor(HomeAccessory): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: - if self.char_level.value != value: - self.char_level.set_value(value) + self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) co_detected = value > THRESHOLD_CO - if self.char_detected.value is not co_detected: - self.char_detected.set_value(co_detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, value) + self.char_detected.set_value(co_detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) @TYPES.register("CarbonDioxideSensor") @@ -232,14 +228,12 @@ class CarbonDioxideSensor(HomeAccessory): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: - if self.char_level.value != value: - self.char_level.set_value(value) + self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) co2_detected = value > THRESHOLD_CO2 - if self.char_detected.value is not co2_detected: - self.char_detected.set_value(co2_detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, value) + self.char_detected.set_value(co2_detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) @TYPES.register("LightSensor") @@ -262,7 +256,7 @@ class LightSensor(HomeAccessory): def async_update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance and self.char_light.value != luminance: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug("%s: Set to %d", self.entity_id, luminance) @@ -297,6 +291,5 @@ class BinarySensor(HomeAccessory): """Update accessory after state change.""" state = new_state.state detected = self.format(state in (STATE_ON, STATE_HOME)) - if self.char_detected.value != detected: - self.char_detected.set_value(detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, detected) + self.char_detected.set_value(detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, detected) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 381110a4e79..3bb496a2abc 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): @@ -89,9 +91,8 @@ class Outlet(HomeAccessory): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state == STATE_ON - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Switch") @@ -121,8 +122,7 @@ class Switch(HomeAccessory): def reset_switch(self, *args): """Reset switch to emulate activate click.""" _LOGGER.debug("%s: Reset switch to off", self.entity_id) - if self.char_on.value is not False: - self.char_on.set_value(False) + self.char_on.set_value(False) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" @@ -141,7 +141,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): @@ -154,9 +154,8 @@ class Switch(HomeAccessory): return current_state = new_state.state == STATE_ON - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Vacuum") @@ -184,9 +183,8 @@ class Vacuum(Switch): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state in (STATE_CLEANING, STATE_ON) - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Valve") @@ -224,9 +222,7 @@ class Valve(HomeAccessory): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = 1 if new_state.state == STATE_ON else 0 - if self.char_active.value != current_state: - _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) - self.char_active.set_value(current_state) - if self.char_in_use.value != current_state: - _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) - self.char_in_use.set_value(current_state) + _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) + self.char_active.set_value(current_state) + _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) + self.char_in_use.set_value(current_state) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index fb3063704c2..c36a32b0d5b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -446,8 +446,7 @@ class Thermostat(HomeAccessory): if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT: homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] if homekit_hvac_mode in self.hc_homekit_to_hass: - if self.char_target_heat_cool.value != homekit_hvac_mode: - self.char_target_heat_cool.set_value(homekit_hvac_mode) + self.char_target_heat_cool.set_value(homekit_hvac_mode) else: _LOGGER.error( "Cannot map hvac target mode: %s to homekit as only %s modes are supported", @@ -459,30 +458,23 @@ class Thermostat(HomeAccessory): hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) if hvac_action: homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] - if self.char_current_heat_cool.value != homekit_hvac_action: - self.char_current_heat_cool.set_value(homekit_hvac_action) + self.char_current_heat_cool.set_value(homekit_hvac_action) # Update current temperature current_temp = _get_current_temperature(new_state, self._unit) - if current_temp is not None and self.char_current_temp.value != current_temp: + if current_temp is not None: self.char_current_temp.set_value(current_temp) # Update current humidity if CHAR_CURRENT_HUMIDITY in self.chars: current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) - if ( - isinstance(current_humdity, (int, float)) - and self.char_current_humidity.value != current_humdity - ): + if isinstance(current_humdity, (int, float)): self.char_current_humidity.set_value(current_humdity) # Update target humidity if CHAR_TARGET_HUMIDITY in self.chars: target_humdity = new_state.attributes.get(ATTR_HUMIDITY) - if ( - isinstance(target_humdity, (int, float)) - and self.char_target_humidity.value != target_humdity - ): + if isinstance(target_humdity, (int, float)): self.char_target_humidity.set_value(target_humdity) # Update cooling threshold temperature if characteristic exists @@ -490,16 +482,14 @@ class Thermostat(HomeAccessory): cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(cooling_thresh, (int, float)): cooling_thresh = self._temperature_to_homekit(cooling_thresh) - if self.char_heating_thresh_temp.value != cooling_thresh: - self.char_cooling_thresh_temp.set_value(cooling_thresh) + self.char_cooling_thresh_temp.set_value(cooling_thresh) # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) if isinstance(heating_thresh, (int, float)): heating_thresh = self._temperature_to_homekit(heating_thresh) - if self.char_heating_thresh_temp.value != heating_thresh: - self.char_heating_thresh_temp.set_value(heating_thresh) + self.char_heating_thresh_temp.set_value(heating_thresh) # Update target temperature target_temp = _get_target_temperature(new_state, self._unit) @@ -515,14 +505,13 @@ class Thermostat(HomeAccessory): temp_high = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(temp_high, (int, float)): target_temp = self._temperature_to_homekit(temp_high) - if target_temp and self.char_target_temp.value != target_temp: + if target_temp: self.char_target_temp.set_value(target_temp) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: unit = UNIT_HASS_TO_HOMEKIT[self._unit] - if self.char_display_units.value != unit: - self.char_display_units.set_value(unit) + self.char_display_units.set_value(unit) @TYPES.register("WaterHeater") @@ -580,7 +569,7 @@ class WaterHeater(HomeAccessory): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) hass_value = HC_HOMEKIT_TO_HASS[value] - if hass_value != HVAC_MODE_HEAT and self.char_target_heat_cool.value != 1: + if hass_value != HVAC_MODE_HEAT: self.char_target_heat_cool.set_value(1) # Heat def set_target_temperature(self, value): @@ -600,28 +589,21 @@ class WaterHeater(HomeAccessory): """Update water_heater state after state change.""" # Update current and target temperature target_temperature = _get_target_temperature(new_state, self._unit) - if ( - target_temperature is not None - and target_temperature != self.char_target_temp.value - ): + if target_temperature is not None: self.char_target_temp.set_value(target_temperature) current_temperature = _get_current_temperature(new_state, self._unit) - if ( - current_temperature is not None - and current_temperature != self.char_current_temp.value - ): + if current_temperature is not None: self.char_current_temp.set_value(current_temperature) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: unit = UNIT_HASS_TO_HOMEKIT[self._unit] - if self.char_display_units.value != unit: - self.char_display_units.set_value(unit) + self.char_display_units.set_value(unit) # Update target operation mode operation_mode = new_state.state - if operation_mode and self.char_target_heat_cool.value != 1: + if operation_mode: self.char_target_heat_cool.set_value(1) # Heat diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 2a162eb2b2a..b4ca2f4918a 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -1,4 +1,6 @@ """Support for HomeKit Controller air quality sensors.""" +import logging + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -7,6 +9,8 @@ from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity +_LOGGER = logging.getLogger(__name__) + AIR_QUALITY_TEXT = { 0: "unknown", 1: "excellent", @@ -20,6 +24,20 @@ AIR_QUALITY_TEXT = { class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): """Representation of a HomeKit Controller Air Quality sensor.""" + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + _LOGGER.warning( + "The homekit_controller air_quality entity has been " + "deprecated and will be removed in 2021.12.0" + ) + await super().async_added_to_hass() + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not to enable this entity by default.""" + # This entity is deprecated, so don't enable by default + return False + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index fc6a5bb4522..a0b15087356 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -1,4 +1,6 @@ """Support for Homekit cameras.""" +from __future__ import annotations + from aiohomekit.model.services import ServicesTypes from homeassistant.components.camera import Camera @@ -21,12 +23,14 @@ class HomeKitCamera(AccessoryEntity, Camera): """Return the current state of the camera.""" return "idle" - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a jpeg with the current camera snapshot.""" return await self._accessory.pairing.image( self._aid, - 640, - 480, + width or 640, + height or 480, ) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 2de80eefd7e..ac4f19dadb4 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -4,12 +4,19 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, @@ -52,7 +59,7 @@ SIMPLE_SENSOR = { "state_class": STATE_CLASS_MEASUREMENT, "unit": PRESSURE_HPA, }, - CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { + CharacteristicsTypes.TEMPERATURE_CURRENT: { "name": "Current Temperature", "device_class": DEVICE_CLASS_TEMPERATURE, "state_class": STATE_CLASS_MEASUREMENT, @@ -62,7 +69,7 @@ SIMPLE_SENSOR = { "probe": lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), }, - CharacteristicsTypes.get_uuid(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT): { + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: { "name": "Current Humidity", "device_class": DEVICE_CLASS_HUMIDITY, "state_class": STATE_CLASS_MEASUREMENT, @@ -72,14 +79,58 @@ SIMPLE_SENSOR = { "probe": lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), }, + CharacteristicsTypes.AIR_QUALITY: { + "name": "Air Quality", + "device_class": DEVICE_CLASS_AQI, + "state_class": STATE_CLASS_MEASUREMENT, + }, + CharacteristicsTypes.DENSITY_PM25: { + "name": "PM2.5 Density", + "device_class": DEVICE_CLASS_PM25, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_PM10: { + "name": "PM10 Density", + "device_class": DEVICE_CLASS_PM10, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_OZONE: { + "name": "Ozone Density", + "device_class": DEVICE_CLASS_OZONE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_NO2: { + "name": "Nitrogen Dioxide Density", + "device_class": DEVICE_CLASS_NITROGEN_DIOXIDE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_SO2: { + "name": "Sulphur Dioxide Density", + "device_class": DEVICE_CLASS_SULPHUR_DIOXIDE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, } +# For legacy reasons, "built-in" characteristic types are in their short form +# And vendor types don't have a short form +# This means long and short forms get mixed up in this dict, and comparisons +# don't work! +# We call get_uuid on *every* type to normalise them to the long form +# Eventually aiohomekit will use the long form exclusively amd this can be removed. +for k, v in list(SIMPLE_SENSOR.items()): + SIMPLE_SENSOR[CharacteristicsTypes.get_uuid(k)] = SIMPLE_SENSOR.pop(k) + class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -96,7 +147,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return HUMIDITY_ICON @property - def state(self): + def native_value(self): """Return the current humidity.""" return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) @@ -105,7 +156,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -122,7 +173,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return TEMP_C_ICON @property - def state(self): + def native_value(self): """Return the current temperature in Celsius.""" return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) @@ -131,7 +182,7 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit light level sensor.""" _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = LIGHT_LUX + _attr_native_unit_of_measurement = LIGHT_LUX def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -148,7 +199,7 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return BRIGHTNESS_ICON @property - def state(self): + def native_value(self): """Return the current light level in lux.""" return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) @@ -157,7 +208,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit Carbon Dioxide sensor.""" _attr_icon = CO2_ICON - _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -169,7 +220,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return f"{super().name} CO2" @property - def state(self): + def native_value(self): """Return the current CO2 level in ppm.""" return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) @@ -178,7 +229,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -229,7 +280,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CHARGING_STATE) == 1 @property - def state(self): + def native_value(self): """Return the current battery level percentage.""" return self.service.value(CharacteristicsTypes.BATTERY_LEVEL) @@ -281,7 +332,7 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): return self._state_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return units for the sensor.""" return self._unit @@ -296,7 +347,7 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): return f"{super().name} - {self._name}" @property - def state(self): + def native_value(self): """Return the current sensor value.""" return self._char.value diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index cd06d12e809..1ad63bfb508 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -21,6 +21,7 @@ "flow_title": "HomeKit tartoz\u00e9k: {name}", "step": { "busy_error": { + "description": "Sz\u00fcntesse meg a p\u00e1ros\u00edt\u00e1st az \u00f6sszes vez\u00e9rl\u0151n, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", "title": "Az eszk\u00f6z m\u00e1r p\u00e1rosul egy m\u00e1sik vez\u00e9rl\u0151vel" }, "max_tries_error": { @@ -36,6 +37,7 @@ "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" }, "protocol_error": { + "description": "El\u0151fordulhat, hogy a k\u00e9sz\u00fcl\u00e9k nincs p\u00e1ros\u00edt\u00e1si m\u00f3dban, \u00e9s sz\u00fcks\u00e9g lehet fizikai vagy virtu\u00e1lis gombnyom\u00e1sra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy az eszk\u00f6z p\u00e1ros\u00edt\u00e1si m\u00f3dban van, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", "title": "Hiba t\u00f6rt\u00e9nt a tartoz\u00e9kkal val\u00f3 kommunik\u00e1ci\u00f3 sor\u00e1n" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/lt.json b/homeassistant/components/homekit_controller/translations/lt.json new file mode 100644 index 00000000000..965b32b366d --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u012erenginio pasirinkimas" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 624050e7146..7da392179f6 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "HomeKit \u4ee3\u7801\u4e0d\u6b63\u786e\u3002\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5\u3002", + "insecure_setup_code": "\u8bf7\u6c42\u7684\u8bbe\u7f6e\u4ee3\u7801\u7531\u4e8e\u8fc7\u4e8e\u7b80\u5355\u800c\u4e0d\u5b89\u5168\u3002\u6b64\u914d\u4ef6\u4e0d\u7b26\u5408\u57fa\u672c\u5b89\u5168\u8981\u6c42\u3002", "max_peers_error": "\u8bbe\u5907\u62d2\u7edd\u914d\u5bf9\uff0c\u56e0\u4e3a\u5b83\u6ca1\u6709\u7a7a\u95f2\u7684\u914d\u5bf9\u5b58\u50a8\u7a7a\u95f4\u3002", "pairing_failed": "\u5c1d\u8bd5\u4e0e\u6b64\u8bbe\u5907\u914d\u5bf9\u65f6\u53d1\u751f\u672a\u5904\u7406\u7684\u9519\u8bef\u3002\u8fd9\u53ef\u80fd\u662f\u6682\u65f6\u6027\u6545\u969c\uff0c\u4e5f\u53ef\u80fd\u662f\u60a8\u7684\u8bbe\u5907\u76ee\u524d\u4e0d\u88ab\u652f\u6301\u3002", "unable_to_pair": "\u65e0\u6cd5\u914d\u5bf9\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "\u5141\u8bb8\u4f7f\u7528\u4e0d\u5b89\u5168\u7684\u8bbe\u7f6e\u4ee3\u7801\u914d\u5bf9\u3002", "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3a XXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 4f1c1d12f81..0880d168375 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -62,6 +62,7 @@ HM_DEVICE_TYPES = { "IPWIODevice", "IPSwitchBattery", "IPMultiIOPCB", + "IPGarageSwitch", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -125,6 +126,7 @@ HM_DEVICE_TYPES = { "TempModuleSTE2", "IPMultiIOPCB", "ValveBoxW", + "CO2SensorIP", ], DISCOVER_CLIMATE: [ "Thermostat", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 8b1ee62a09e..f500ef54b56 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.73"], + "requirements": ["pyhomematic==0.1.74"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index ad62001d5f9..18690ac3553 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -3,7 +3,9 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, DEGREE, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -72,6 +74,7 @@ HM_UNIT_HA_CAST = { "VALVE_STATE": PERCENTAGE, "CARRIER_SENSE_LEVEL": PERCENTAGE, "DUTY_CYCLE_LEVEL": PERCENTAGE, + "CONCENTRATION": CONCENTRATION_PARTS_PER_MILLION, } HM_DEVICE_CLASS_HA_CAST = { @@ -85,6 +88,7 @@ HM_DEVICE_CLASS_HA_CAST = { "HIGHEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, "POWER": DEVICE_CLASS_POWER, "CURRENT": DEVICE_CLASS_POWER, + "CONCENTRATION": DEVICE_CLASS_CO2, } HM_ICON_HA_CAST = {"WIND_SPEED": "mdi:weather-windy", "BRIGHTNESS": "mdi:invert-colors"} @@ -107,7 +111,7 @@ class HMSensor(HMDevice, SensorEntity): """Representation of a HomeMatic sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" # Does a cast exist for this class? name = self._hmdevice.__class__.__name__ @@ -118,7 +122,7 @@ class HMSensor(HMDevice, SensorEntity): return self._hm_get_state() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return HM_UNIT_HA_CAST.get(self._state) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 475df8ec2af..df8ed33ded0 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -137,12 +137,12 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): return "mdi:access-point-network" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the access point.""" return self._device.dutyCycleLevel @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -164,14 +164,14 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): return "mdi:radiator" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: return self._device.valveState return round(self._device.valvePosition * 100) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -189,12 +189,12 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_HUMIDITY @property - def state(self) -> int: + def native_value(self) -> int: """Return the state.""" return self._device.humidity @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -212,7 +212,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def state(self) -> float: + def native_value(self) -> float: """Return the state.""" if hasattr(self._device, "valveActualTemperature"): return self._device.valveActualTemperature @@ -220,7 +220,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): return self._device.actualTemperature @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return TEMP_CELSIUS @@ -249,7 +249,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_ILLUMINANCE @property - def state(self) -> float: + def native_value(self) -> float: """Return the state.""" if hasattr(self._device, "averageIllumination"): return self._device.averageIllumination @@ -257,7 +257,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): return self._device.illumination @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return LIGHT_LUX @@ -287,12 +287,12 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_POWER @property - def state(self) -> float: + def native_value(self) -> float: """Return the power consumption value.""" return self._device.currentPowerConsumption @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return POWER_WATT @@ -305,12 +305,12 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Windspeed") @property - def state(self) -> float: + def native_value(self) -> float: """Return the wind speed value.""" return self._device.windSpeed @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return SPEED_KILOMETERS_PER_HOUR @@ -338,12 +338,12 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Today Rain") @property - def state(self) -> float: + def native_value(self) -> float: """Return the today's rain value.""" return round(self._device.todayRainCounter, 2) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return LENGTH_MILLIMETERS @@ -352,7 +352,7 @@ class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEnt """Representation of the HomematicIP passage detector delta counter.""" @property - def state(self) -> int: + def native_value(self) -> int: """Return the passage detector delta counter value.""" return self._device.leftRightCounterDelta diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json new file mode 100644 index 00000000000..41534be9d8d --- /dev/null +++ b/homeassistant/components/honeywell/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json new file mode 100644 index 00000000000..5583dc22f2e --- /dev/null +++ b/homeassistant/components/honeywell/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/no.json b/homeassistant/components/honeywell/translations/no.json new file mode 100644 index 00000000000..97d31d34961 --- /dev/null +++ b/homeassistant/components/honeywell/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn legitimasjonen som brukes for \u00e5 logge deg p\u00e5 mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/zh-Hans.json b/homeassistant/components/honeywell/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/honeywell/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 297bfa5264f..5a44a2937e8 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -133,12 +133,12 @@ class HpIloSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 18bc51af1d1..6dd2d9adb8a 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -63,12 +63,24 @@ def async_setup_forwarded( an HTTP 400 status code is thrown. """ + 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 + @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 +132,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/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index ccbe6a31de2..4f93ecbc42d 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -98,12 +98,12 @@ class HTU21DSensor(SensorEntity): return self._name @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 81b715b71fe..ec9281659f5 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -185,11 +185,6 @@ class Router: _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) try: self.data[key] = func() - except ResponseErrorNotSupportedException: - _LOGGER.info( - "%s not supported by device, excluding from future updates", key - ) - self.subscriptions.pop(key) except ResponseErrorLoginRequiredException: if isinstance(self.connection, AuthorizedConnection): _LOGGER.debug("Trying to authorize again") @@ -206,7 +201,13 @@ class Router: ) self.subscriptions.pop(key) except ResponseErrorException as exc: - if exc.code != -1: + if not isinstance( + exc, ResponseErrorNotSupportedException + ) and exc.code not in ( + # additional codes treated as unusupported + -1, + 100006, + ): raise _LOGGER.info( "%s apparently not supported by device, excluding from future updates", @@ -298,7 +299,7 @@ class Router: class HuaweiLteData: """Shared state.""" - hass_config: dict = attr.ib() + hass_config: ConfigType = attr.ib() # Our YAML config, keyed by router URL config: dict[str, dict[str, Any]] = attr.ib() routers: dict[str, Router] = attr.ib(init=False, factory=dict) @@ -665,9 +666,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: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 4340d5912c9..47987e5607e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -426,7 +426,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return f"{self.key}.{self.item}" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return sensor state.""" return self._state @@ -436,7 +436,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return self.meta.device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return sensor's unit of measurement.""" return self.meta.unit or self._unit diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index eff9c8a813b..22bd37c37ba 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -11,6 +11,8 @@ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_url": "\u00c9rv\u00e9nytelen URL", + "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb", + "response_error": "Ismeretlen hiba az eszk\u00f6zr\u0151l", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name}", @@ -21,6 +23,7 @@ "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Adja meg az eszk\u00f6z hozz\u00e1f\u00e9r\u00e9si adatait.", "title": "Huawei LTE konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index a328858c57f..3c8b26ab0cd 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Brukernavn" }, - "description": "Fyll inn detaljer for enhetstilgang. Spesifisering av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integrasjonsfunksjoner. P\u00e5 en annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integrasjonen er aktiv, og omvendt.", + "description": "Angi enhetsadgangsdetaljer.", "title": "Konfigurer Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", "track_new_devices": "Spor nye enheter", - "track_wired_clients": "Spor kablede nettverksklienter" + "track_wired_clients": "Spor kablede nettverksklienter", + "unauthenticated_mode": "Uautentisert modus (endring krever omlasting)" } } } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index 987c53e4d5c..4fb447403d6 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "error": { - "incorrect_username": "\u7528\u6237\u540d\u9519\u8bef" + "incorrect_username": "\u7528\u6237\u540d\u9519\u8bef", + "login_attempts_exceeded": "\u5df2\u8d85\u8fc7\u6700\u5927\u767b\u5f55\u6b21\u6570\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index a512012bc68..80658fff21e 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -42,10 +42,10 @@ class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = LIGHT_LUX + _attr_native_unit_of_measurement = LIGHT_LUX @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.sensor.lightlevel is None: return None @@ -78,10 +78,10 @@ class HueTemperature(GenericHueGaugeSensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.sensor.temperature is None: return None @@ -94,7 +94,7 @@ class HueBattery(GenericHueSensor, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def unique_id(self): @@ -102,7 +102,7 @@ class HueBattery(GenericHueSensor, SensorEntity): return f"{self.sensor.uniqueid}-battery" @property - def state(self): + def native_value(self): """Return the state of the battery.""" return self.sensor.battery diff --git a/homeassistant/components/hue/translations/da.json b/homeassistant/components/hue/translations/da.json index 031076172ac..f081a912dd7 100644 --- a/homeassistant/components/hue/translations/da.json +++ b/homeassistant/components/hue/translations/da.json @@ -2,22 +2,22 @@ "config": { "abort": { "all_configured": "Alle Philips Hue-broer er allerede konfigureret", - "already_configured": "Bridgen er allerede konfigureret", + "already_configured": "Enhed er allerede konfigureret", "already_in_progress": "Bro-konfiguration er allerede i gang.", - "cannot_connect": "Kunne ikke oprette forbindelse til bridgen", + "cannot_connect": "Kunne ikke oprette forbindelse", "discover_timeout": "Ingen Philips Hue-bro fundet", "no_bridges": "Ingen Philips Hue-broer fundet", "not_hue_bridge": "Ikke en Hue-bro", - "unknown": "Ukendt fejl opstod" + "unknown": "Uventet fejl" }, "error": { - "linking": "Der opstod en ukendt linkfejl.", + "linking": "Der opstod en uventet fejl", "register_failed": "Det lykkedes ikke at registrere, pr\u00f8v igen" }, "step": { "init": { "data": { - "host": "V\u00e6rt" + "host": "Server" }, "title": "V\u00e6lg Hue bridge" }, diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index d0aa043b10b..30084ee9940 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -35,12 +35,33 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "double_buttons_1_3": "Els\u0151 \u00e9s harmadik gomb", + "double_buttons_2_4": "M\u00e1sodik \u00e9s negyedik gomb", "turn_off": "Kikapcsol\u00e1s", "turn_on": "Bekapcsol\u00e1s" }, "trigger_type": { + "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", - "remote_button_short_release": "\"{subtype}\" gomb elengedve" + "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_double_button_long_press": "Mindk\u00e9t \"{subtype}\" hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en megjelent", + "remote_double_button_short_press": "Mindk\u00e9t \"{subtype}\" megjelent" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Hue csoportok enged\u00e9lyez\u00e9se", + "allow_unreachable": "Hagyja, hogy az el\u00e9rhetetlen izz\u00f3k helyesen jelents\u00e9k \u00e1llapotukat" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/lt.json b/homeassistant/components/hue/translations/lt.json new file mode 100644 index 00000000000..1e12894085b --- /dev/null +++ b/homeassistant/components/hue/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "Hostas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 3cda3cdec00..6f18ad27796 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -75,7 +75,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data[self._source_type][self._sensor_type] is not None: return round( @@ -85,7 +85,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return None @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement 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]), } diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index d66671fe1ea..14501a9c528 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -50,7 +50,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Representation of an shade battery charge sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @@ -70,7 +70,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): return f"{self._unique_id}_charge" @property - def state(self): + def native_value(self): """Get the current value in percentage.""" return round( self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100 diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index 3de1b9d0117..1fedd8bc126 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -9,10 +9,15 @@ }, "flow_title": "{name} ({host})", "step": { + "link": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "Csatlakozzon a PowerView Hubhoz" + }, "user": { "data": { "host": "IP c\u00edm" - } + }, + "title": "Csatlakozzon a PowerView Hubhoz" } } } diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index a3df466da74..8a188f7dde8 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -177,7 +177,7 @@ class HVVDepartureSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json index deab9bcb929..dfbdd92f27a 100644 --- a/homeassistant/components/hvv_departures/translations/hu.json +++ b/homeassistant/components/hvv_departures/translations/hu.json @@ -5,15 +5,29 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_results": "Nincs eredm\u00e9ny. Pr\u00f3b\u00e1lja ki m\u00e1sik \u00e1llom\u00e1ssal/c\u00edmmel" }, "step": { + "station": { + "data": { + "station": "\u00c1llom\u00e1s/c\u00edm" + }, + "title": "Adja meg az \u00e1llom\u00e1st/c\u00edmet" + }, + "station_select": { + "data": { + "station": "\u00c1llom\u00e1s/c\u00edm" + }, + "title": "\u00c1llom\u00e1s/c\u00edm kiv\u00e1laszt\u00e1sa" + }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a HVV API-hoz" } } }, @@ -21,8 +35,11 @@ "step": { "init": { "data": { - "offset": "Eltol\u00e1s (perc)" + "filter": "V\u00e1lassza ki a sorokat", + "offset": "Eltol\u00e1s (perc)", + "real_time": "Val\u00f3s idej\u0171 adatok haszn\u00e1lata" }, + "description": "M\u00f3dos\u00edtsa az indul\u00e1si \u00e9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sait", "title": "Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 62108afbded..0e9afb6d729 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -40,12 +40,12 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): """A sensor implementation for Hydrawise device.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return DEVICE_MAP[self._sensor_type][ DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX") diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 22134400a45..809449543af 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -210,7 +210,9 @@ class HyperionCamera(Camera): finally: await self._stop_image_streaming_for_client() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return single camera image bytes.""" async with self._image_streaming() as is_streaming: if is_streaming: diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index b1882619fda..de0e76fc3aa 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -86,7 +86,7 @@ class IamMeter(CoordinatorEntity, SensorEntity): self.dev_name = dev_name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data.data[self.sensor_name] @@ -106,6 +106,6 @@ class IamMeter(CoordinatorEntity, SensorEntity): return "mdi:flash" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.unit diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index ae32db9eb9e..61e4560c3be 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -31,7 +31,7 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): return self.dev.label @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the measurement unit for the sensor.""" if self.dev.name.endswith("_temp"): if self.dev.system.temp_unit == "F": @@ -40,7 +40,7 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): return None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if self.dev.state == "": return None diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index dcb7b906ee3..1ca85c41190 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -11,7 +11,9 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kja felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "title": "Csatlakoz\u00e1s az iAqualinkhez" } } } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index ec55a1fcedd..5469eadc998 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -54,7 +54,7 @@ class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" @@ -73,7 +73,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return f"{self._device.name} battery state" @property - def state(self) -> int: + def native_value(self) -> int: """Battery state percentage.""" return self._device.battery_level diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index bb47cdd879b..722b3711e67 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_device": "Egyik k\u00e9sz\u00fcl\u00e9ke sem aktiv\u00e1lta az \"iPhone keres\u00e9se\" funkci\u00f3t", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -14,6 +15,7 @@ "data": { "password": "Jelsz\u00f3" }, + "description": "A(z) {username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "trusted_device": { @@ -26,7 +28,8 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "E-mail" + "username": "E-mail", + "with_family": "Csal\u00e1ddal" }, "description": "Adja meg hiteles\u00edt\u0151 adatait", "title": "iCloud hiteles\u00edt\u0151 adatok" diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json index 5184e89f29a..216511c62f5 100644 --- a/homeassistant/components/ifttt/translations/de.json +++ b/homeassistant/components/ifttt/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 Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." + "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn [der Dokumentation] ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/zh-Hans.json b/homeassistant/components/ifttt/translations/zh-Hans.json index c9e8bfd6044..78cbc37a7d9 100644 --- a/homeassistant/components/ifttt/translations/zh-Hans.json +++ b/homeassistant/components/ifttt/translations/zh-Hans.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u5b9e\u4f8b\u5df2\u914d\u7f6e\uff0c\u4e14\u53ea\u80fd\u5b58\u5728\u5355\u4e2a\u914d\u7f6e\u3002", + "webhook_not_internet_accessible": "Home Assistant \u9700\u8981\u7f51\u7edc\u8fde\u63a5\u4ee5\u83b7\u53d6\u76f8\u5173\u63a8\u9001\u4fe1\u606f\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook applet]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" }, diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index d1aec781df7..17c17980c95 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -48,12 +48,12 @@ class IHCSensor(IHCDevice, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index e27abf70127..51263e38ab7 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -37,7 +38,7 @@ UPDATE_FIELDS = { } -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Image integration.""" image_dir = pathlib.Path(hass.config.path(DOMAIN)) hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 4158d1be801..c3d6b2198ce 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -95,7 +95,7 @@ class ImapSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the number of emails found.""" return self._email_count diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index cdd47d68d76..87c18a56bbe 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -165,7 +165,7 @@ class EmailContentSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the current email state.""" return self._message diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index a9e1faaba10..9fb99321ff2 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -59,7 +59,7 @@ class IncomfortSensor(IncomfortChild, SensorEntity): self._unit_of_measurement = None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._heater.status[self._state_attr] @@ -69,7 +69,7 @@ class IncomfortSensor(IncomfortChild, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index c2cb5070a4c..bdbfafaf790 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -222,12 +222,12 @@ class InfluxSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/insteon/translations/fa.json b/homeassistant/components/insteon/translations/fa.json new file mode 100644 index 00000000000..2456fbcba00 --- /dev/null +++ b/homeassistant/components/insteon/translations/fa.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0628\u0647 \u062f\u0631\u0633\u062a\u06cc \u062a\u0646\u0638\u06cc\u0645 \u0634\u062f\u0647 \u0627\u0633\u062a. \u062a\u0646\u0647\u0627 \u06cc\u06a9 \u062a\u0646\u0638\u06cc\u0645 \u0627\u0645\u06a9\u0627\u0646 \u067e\u0630\u06cc\u0631 \u0627\u0633\u062a." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index 462fae3e1cb..8444aa97655 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -31,6 +31,7 @@ "data": { "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, + "description": "Konfigur\u00e1lja az Insteon PowerLink modemet (PLM).", "title": "Insteon PLM" }, "user": { @@ -44,16 +45,28 @@ }, "options": { "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "input_error": "\u00c9rv\u00e9nytelen bejegyz\u00e9sek, ellen\u0151rizze \u00e9rt\u00e9keket.", + "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get" }, "step": { "add_override": { + "data": { + "address": "Eszk\u00f6z c\u00edme (azaz 1a2b3c)", + "cat": "Eszk\u00f6zkateg\u00f3ria (azaz 0x10)", + "subcat": "Eszk\u00f6z alkateg\u00f3ria (azaz 0x0a)" + }, + "description": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa.", "title": "Insteon" }, "add_x10": { "data": { + "housecode": "H\u00e1zk\u00f3d (a - p)", + "platform": "Platform", + "steps": "F\u00e9nyer\u0151-szab\u00e1lyoz\u00e1si l\u00e9p\u00e9sek (csak k\u00f6nny\u0171 eszk\u00f6z\u00f6k eset\u00e9n, alap\u00e9rtelmezett 22)", "unitcode": "Egys\u00e9gk\u00f3d (1 - 16)" }, + "description": "M\u00f3dos\u00edtsa az Insteon Hub jelszav\u00e1t.", "title": "Insteon" }, "change_hub_config": { @@ -63,15 +76,25 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "M\u00f3dos\u00edtsa az Insteon Hub csatlakoz\u00e1si adatait. A m\u00f3dos\u00edt\u00e1s elv\u00e9gz\u00e9se ut\u00e1n \u00fajra kell ind\u00edtania a Home Assistant alkalmaz\u00e1st. Ez nem v\u00e1ltoztatja meg a Hub konfigur\u00e1ci\u00f3j\u00e1t. A Hub konfigur\u00e1ci\u00f3j\u00e1nak m\u00f3dos\u00edt\u00e1s\u00e1hoz haszn\u00e1lja a Hub alkalmaz\u00e1st.", "title": "Insteon" }, "init": { "data": { - "add_x10": "Adjon hozz\u00e1 egy X10 eszk\u00f6zt." + "add_override": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa.", + "add_x10": "Adjon hozz\u00e1 egy X10 eszk\u00f6zt.", + "change_hub_config": "M\u00f3dos\u00edtsa a Hub konfigur\u00e1ci\u00f3j\u00e1t.", + "remove_override": "Egy eszk\u00f6z fel\u00fclb\u00edr\u00e1lat\u00e1nak elt\u00e1vol\u00edt\u00e1sa.", + "remove_x10": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt." }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edt\u00e1st.", "title": "Insteon" }, "remove_override": { + "data": { + "address": "V\u00e1lassza ki az elt\u00e1vol\u00edtani k\u00edv\u00e1nt eszk\u00f6z c\u00edm\u00e9t" + }, + "description": "T\u00e1vol\u00edtsa el az eszk\u00f6z fel\u00fclb\u00edr\u00e1l\u00e1s\u00e1t", "title": "Insteon" }, "remove_x10": { diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 2b7d89decea..b8e72c3be5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -5,11 +5,10 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - ATTR_LAST_RESET, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -28,7 +27,6 @@ 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 @@ -124,38 +122,29 @@ 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 + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING 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 (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) + self._unit_of_measurement = state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + @callback def calc_integration(event): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") - if ( - old_state is None - or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - ): - return if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -167,6 +156,14 @@ class IntegrationSensor(RestoreEntity, SensorEntity): and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER ): self._attr_device_class = DEVICE_CLASS_ENERGY + + if ( + old_state is None + or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + return + try: # integration as the Riemann integral of previous measures. area = 0 @@ -209,12 +206,12 @@ class IntegrationSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 4fd6daa5102..d626daa8c3b 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -6,11 +6,12 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv, integration_platform, intent +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index c1442f0de9f..c3c1ad2b8ce 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,6 +1,8 @@ """Support for Home Assistant iOS app sensors.""" +from __future__ import annotations + from homeassistant.components import ios -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,10 +10,17 @@ from homeassistant.helpers.icon import icon_for_battery_level from .const import DOMAIN -SENSOR_TYPES = { - "level": ["Battery Level", PERCENTAGE], - "state": ["Battery State", None], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="level", + name="Battery Level", + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="state", + name="Battery State", + ), +) DEFAULT_ICON_LEVEL = "mdi:battery" DEFAULT_ICON_STATE = "mdi:power-plug" @@ -24,25 +33,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up iOS from a config entry.""" - dev = [] - for device_name, device in ios.devices(hass).items(): - for sensor_type in ("level", "state"): - dev.append(IOSSensor(sensor_type, device_name, device)) + entities = [ + IOSSensor(device_name, device, description) + for device_name, device in ios.devices(hass).items() + for description in SENSOR_TYPES + ] - async_add_entities(dev, True) + async_add_entities(entities, True) class IOSSensor(SensorEntity): """Representation of an iOS sensor.""" - def __init__(self, sensor_type, device_name, device): + _attr_should_poll = False + + def __init__(self, device_name, device, description: SensorEntityDescription): """Initialize the sensor.""" - self._device_name = device_name - self._name = f"{device_name} {SENSOR_TYPES[sensor_type][0]}" + self.entity_description = description self._device = device - self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + device_name = device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME] + self._attr_name = f"{device_name} {description.key}" + + device_id = device[ios.ATTR_DEVICE_ID] + self._attr_unique_id = f"{description.key}_{device_id}" @property def device_info(self): @@ -60,33 +74,6 @@ class IOSSensor(SensorEntity): "sw_version": self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION], } - @property - def name(self): - """Return the name of the iOS sensor.""" - device_name = self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME] - return f"{device_name} {SENSOR_TYPES[self.type][0]}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - device_id = self._device[ios.ATTR_DEVICE_ID] - return f"{self.type}_{device_id}" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - @property def extra_state_attributes(self): """Return the device state attributes.""" @@ -119,7 +106,7 @@ class IOSSensor(SensorEntity): charging = False icon_state = f"{DEFAULT_ICON_LEVEL}-unknown" - if self.type == "state": + if self.entity_description.key == "state": return icon_state return icon_for_battery_level(battery_level=battery_level, charging=charging) @@ -127,12 +114,16 @@ class IOSSensor(SensorEntity): def _update(self, device): """Get the latest state of the sensor.""" self._device = device - self._state = self._device[ios.ATTR_BATTERY][self.type] + self._attr_native_value = self._device[ios.ATTR_BATTERY][ + self.entity_description.key + ] self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" - self._state = self._device[ios.ATTR_BATTERY][self.type] + self._attr_native_value = self._device[ios.ATTR_BATTERY][ + self.entity_description.key + ] device_id = self._device[ios.ATTR_DEVICE_ID] self.async_on_remove( async_dispatcher_connect(self.hass, f"{DOMAIN}.{device_id}", self._update) diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py index 62260be2410..687a4ca35d6 100644 --- a/homeassistant/components/iota/sensor.py +++ b/homeassistant/components/iota/sensor.py @@ -47,12 +47,12 @@ class IotaBalanceSensor(IotaDevice, SensorEntity): return f"{self._name} Balance" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "IOTA" @@ -81,7 +81,7 @@ class IotaNodeSensor(IotaDevice, SensorEntity): return "IOTA Node" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 610ff91250f..07b9cc069e4 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -41,12 +41,12 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 5d736c864e1..e7c0d5c38f5 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -72,7 +72,7 @@ class IPPSensor(IPPEntity, SensorEntity): """Initialize IPP sensor.""" self._key = key self._attr_unique_id = f"{unique_id}_{key}" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement super().__init__( entry_id=entry_id, @@ -123,7 +123,7 @@ class IPPMarkerSensor(IPPSensor): } @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" level = self.coordinator.data.markers[self.marker_index].level @@ -164,7 +164,7 @@ class IPPPrinterSensor(IPPSensor): } @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.coordinator.data.state.printer_state @@ -189,7 +189,7 @@ class IPPUptimeSensor(IPPSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 8c988eff551..a024cfb2e56 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges." + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges.", + "ipp_error": "IPP hiba t\u00f6rt\u00e9nt.", + "ipp_version_error": "A nyomtat\u00f3 nem t\u00e1mogatja az IPP verzi\u00f3t.", + "parse_error": "Nem siker\u00fclt elemezni a nyomtat\u00f3 v\u00e1lasz\u00e1t.", + "unique_id_required": "Az eszk\u00f6zb\u0151l hi\u00e1nyzik a felfedez\u00e9shez sz\u00fcks\u00e9ges egyedi azonos\u00edt\u00f3." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -13,14 +17,18 @@ "step": { "user": { "data": { + "base_path": "Relat\u00edv \u00fatvonal a nyomtat\u00f3hoz", "host": "Hoszt", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "description": "\u00c1ll\u00edtsa be a nyomtat\u00f3t az Internet Printing Protocol (IPP) protokollon kereszt\u00fcl, hogy integr\u00e1lhat\u00f3 legyen a Home Assistant seg\u00edts\u00e9g\u00e9vel.", + "title": "Kapcsolja \u00f6ssze a nyomtat\u00f3t" }, "zeroconf_confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", + "title": "Felfedezett nyomtat\u00f3" } } } diff --git a/homeassistant/components/ipp/translations/zh-Hans.json b/homeassistant/components/ipp/translations/zh-Hans.json index 254f6df9327..38242cae563 100644 --- a/homeassistant/components/ipp/translations/zh-Hans.json +++ b/homeassistant/components/ipp/translations/zh-Hans.json @@ -1,10 +1,25 @@ { "config": { "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "ipp_version_error": "\u6253\u5370\u673a\u4e0d\u652f\u6301\u8be5 IPP \u7248\u672c", + "parse_error": "\u65e0\u6cd5\u89e3\u6790\u6253\u5370\u673a\u54cd\u5e94\u3002" }, "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "connection_upgrade": "\u65e0\u6cd5\u8fde\u63a5\u5230\u6253\u5370\u673a\u3002\u8bf7\u9009\u4e2d SSL/TLS \u9009\u9879\u540e\u91cd\u8bd5\u3002" + }, + "step": { + "user": { + "data": { + "base_path": "\u6253\u5370\u673a\u7684\u76f8\u5bf9\u8def\u5f84" + }, + "description": "\u901a\u8fc7 Internet \u6253\u5370\u534f\u8bae (IPP) \u8bbe\u7f6e\u60a8\u7684\u6253\u5370\u673a\uff0c\u4e0e Home Assistant \u8fde\u63a5\u3002", + "title": "\u8fde\u63a5\u60a8\u7684\u6253\u5370\u673a" + }, + "zeroconf_confirm": { + "title": "\u5df2\u53d1\u73b0\u7684\u6253\u5370\u673a" + } } } } \ No newline at end of file diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index fa783cc9031..37cc7bedb71 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -109,7 +109,7 @@ class IQVIAEntity(CoordinatorEntity, SensorEntity): self._attr_icon = icon self._attr_name = name self._attr_unique_id = f"{entry.data[CONF_ZIP_CODE]}_{sensor_type}" - self._attr_unit_of_measurement = "index" + self._attr_native_unit_of_measurement = "index" self._entry = entry self._type = sensor_type diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 0ff236a8f79..10d33bfb4bf 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -104,7 +104,7 @@ class ForecastSensor(IQVIAEntity): @callback def update_from_latest_data(self): """Update the sensor.""" - if not self.coordinator.data: + if not self.available: return data = self.coordinator.data.get("Location", {}) @@ -120,6 +120,7 @@ class ForecastSensor(IQVIAEntity): if i["minimum"] <= average <= i["maximum"] ] + self._attr_native_value = average self._attr_extra_state_attributes.update( { ATTR_CITY: data["City"].title(), @@ -134,6 +135,10 @@ class ForecastSensor(IQVIAEntity): outlook_coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR][ self._entry.entry_id ][TYPE_ALLERGY_OUTLOOK] + + if not outlook_coordinator.last_update_success: + return + self._attr_extra_state_attributes[ ATTR_OUTLOOK ] = outlook_coordinator.data.get("Outlook") @@ -141,8 +146,6 @@ class ForecastSensor(IQVIAEntity): ATTR_SEASON ] = outlook_coordinator.data.get("Season") - self._attr_state = average - class IndexSensor(IQVIAEntity): """Define sensor related to indices.""" @@ -210,4 +213,4 @@ class IndexSensor(IQVIAEntity): f"{attrs['Name'].lower()}_index" ] = attrs["Index"] - self._attr_state = period["Index"] + self._attr_native_value = period["Index"] diff --git a/homeassistant/components/iqvia/translations/hu.json b/homeassistant/components/iqvia/translations/hu.json index f5301e874ea..0ae420e47aa 100644 --- a/homeassistant/components/iqvia/translations/hu.json +++ b/homeassistant/components/iqvia/translations/hu.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_zip_code": "Az ir\u00e1ny\u00edt\u00f3sz\u00e1m \u00e9rv\u00e9nytelen" + }, + "step": { + "user": { + "data": { + "zip_code": "Ir\u00e1ny\u00edt\u00f3sz\u00e1m" + }, + "description": "T\u00f6ltse ki amerikai vagy kanadai ir\u00e1ny\u00edt\u00f3sz\u00e1m\u00e1t.", + "title": "IQVIA" + } } } } \ No newline at end of file diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index b5ba16f8541..9ec28d73836 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -83,7 +83,7 @@ class IrishRailTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -114,7 +114,7 @@ class IrishRailTransportSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 2fa563785d2..99cc65bb548 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -43,7 +43,7 @@ class IslamicPrayerTimeSensor(SensorEntity): return self.sensor_type @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self.client.prayer_times_info.get(self.sensor_type) diff --git a/homeassistant/components/islamic_prayer_times/translations/hu.json b/homeassistant/components/islamic_prayer_times/translations/hu.json index 065747fb39d..5bad8174b9a 100644 --- a/homeassistant/components/islamic_prayer_times/translations/hu.json +++ b/homeassistant/components/islamic_prayer_times/translations/hu.json @@ -2,6 +2,22 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iszl\u00e1m imaid\u0151ket?", + "title": "\u00c1ll\u00edtsa be az iszl\u00e1m imaid\u0151t" + } } - } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Az ima sz\u00e1m\u00edt\u00e1si m\u00f3dszere" + } + } + } + }, + "title": "Iszl\u00e1m ima id\u0151k" } \ No newline at end of file diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index ebf32384d85..f12f3cb6bdd 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -67,7 +67,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return UOM_FRIENDLY_NAME.get(uom) @property - def state(self) -> str: + def native_value(self) -> str: """Get the state of the ISY994 sensor device.""" value = self._node.status if value == ISY_VALUE_UNKNOWN: @@ -97,7 +97,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return value @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Get the Home Assistant unit of measurement for the device.""" raw_units = self.raw_unit_of_measurement # Check if this is a known index pair UOM @@ -117,7 +117,7 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): self._name = vname @property - def state(self): + def native_value(self): """Return the state of the variable.""" return convert_isy_value_to_hass(self._node.status, "", self._node.prec) diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 065be706d0f..dab85300e6d 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_host": "A gazdag\u00e9p bejegyz\u00e9se nem volt teljes URL-form\u00e1tumban, p\u00e9ld\u00e1ul: http://192.168.10.100:80", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", @@ -14,14 +15,24 @@ "data": { "host": "URL", "password": "Jelsz\u00f3", + "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A gazdag\u00e9p bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", + "title": "Csatlakozzon az ISY994-hez" } } }, "options": { "step": { "init": { + "data": { + "ignore_string": "Figyelmen k\u00edv\u00fcl hagyja a karakterl\u00e1ncot", + "restore_light_state": "F\u00e9nyer\u0151 vissza\u00e1ll\u00edt\u00e1sa", + "sensor_string": "Csom\u00f3pont \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc", + "variable_sensor_string": "V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc" + }, + "description": "\u00c1ll\u00edtsa be az ISY integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sait:\n \u2022 Csom\u00f3pont -\u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely eszk\u00f6z vagy mappa, amelynek nev\u00e9ben \u201eNode Sensor String\u201d szerepel, \u00e9rz\u00e9kel\u0151k\u00e9nt vagy bin\u00e1ris \u00e9rz\u00e9kel\u0151k\u00e9nt fog kezelni.\n \u2022 Karakterl\u00e1nc figyelmen k\u00edv\u00fcl hagy\u00e1sa: Minden olyan eszk\u00f6z, amelynek a neve \u201eIgnore String\u201d, figyelmen k\u00edv\u00fcl marad.\n \u2022 V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely v\u00e1ltoz\u00f3, amely tartalmazza a \u201eV\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1ncot\u201d, hozz\u00e1ad\u00f3dik \u00e9rz\u00e9kel\u0151k\u00e9nt.\n \u2022 F\u00e9nyer\u0151ss\u00e9g vissza\u00e1ll\u00edt\u00e1sa: Ha enged\u00e9lyezve van, akkor az el\u0151z\u0151 f\u00e9nyer\u0151 vissza\u00e1ll, amikor a k\u00e9sz\u00fcl\u00e9ket be\u00e9p\u00edtett On-Level helyett bekapcsolja.", "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 76744550649..e3f4b62af63 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the iZone component config.""" conf = config.get(IZONE) if not conf: diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 17a61c932a3..e3f51ea5e2c 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -50,7 +50,7 @@ class JewishCalendarSensor(SensorEntity): self._holiday_attrs = {} @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if isinstance(self._state, datetime): return self._state.isoformat() @@ -134,7 +134,7 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): _attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._state is None: return None diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 38089a6e17f..0480eac80b3 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR @@ -30,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the JuiceNet component.""" conf = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 759979c5f11..9b1def3b678 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -14,11 +14,6 @@ class JuiceNetDevice(CoordinatorEntity): self.device = device self.type = sensor_type - @property - def name(self): - """Return the name of the device.""" - return self.device.name - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 51792daf38c..4eaaba41b55 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,5 +1,11 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from __future__ import annotations + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -17,61 +23,81 @@ from homeassistant.const import ( from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice -SENSOR_TYPES = { - "status": ["Charging Status", None, None, None], - "temperature": [ - "Temperature", - TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - ], - "voltage": ["Voltage", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, None], - "amps": [ - "Amps", - ELECTRIC_CURRENT_AMPERE, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, - ], - "watts": ["Watts", POWER_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], - "charge_time": ["Charge time", TIME_SECONDS, None, None], - "energy_added": ["Energy added", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, None], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="status", + name="Charging Status", + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage", + name="Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + SensorEntityDescription( + key="amps", + name="Amps", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="watts", + name="Watts", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="charge_time", + name="Charge time", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + SensorEntityDescription( + key="energy_added", + name="Energy added", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the JuiceNet Sensors.""" - entities = [] juicenet_data = hass.data[DOMAIN][config_entry.entry_id] api = juicenet_data[JUICENET_API] coordinator = juicenet_data[JUICENET_COORDINATOR] - for device in api.devices: - for sensor in SENSOR_TYPES: - entities.append(JuiceNetSensorDevice(device, sensor, coordinator)) + entities = [ + JuiceNetSensorDevice(device, coordinator, description) + for device in api.devices + for description in SENSOR_TYPES + ] async_add_entities(entities) class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): """Implementation of a JuiceNet sensor.""" - def __init__(self, device, sensor_type, coordinator): + def __init__(self, device, coordinator, description: SensorEntityDescription): """Initialise the sensor.""" - super().__init__(device, sensor_type, coordinator) - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._attr_device_class = SENSOR_TYPES[sensor_type][2] - self._attr_state_class = SENSOR_TYPES[sensor_type][3] - - @property - def name(self): - """Return the name of the device.""" - return f"{self.device.name} {self._name}" + super().__init__(device, description.key, coordinator) + self.entity_description = description + self._attr_name = f"{self.device.name} {description.name}" @property def icon(self): """Return the icon of the sensor.""" icon = None - if self.type == "status": + if self.entity_description.key == "status": status = self.device.status if status == "standby": icon = "mdi:power-plug-off" @@ -79,43 +105,11 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): icon = "mdi:power-plug" elif status == "charging": icon = "mdi:battery-positive" - elif self.type == "temperature": - icon = "mdi:thermometer" - elif self.type == "voltage": - icon = "mdi:flash" - elif self.type == "amps": - icon = "mdi:flash" - elif self.type == "watts": - icon = "mdi:flash" - elif self.type == "charge_time": - icon = "mdi:timer-outline" - elif self.type == "energy_added": - icon = "mdi:flash" + else: + icon = self.entity_description.icon return icon @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def state(self): + def native_value(self): """Return the state.""" - state = None - if self.type == "status": - state = self.device.status - elif self.type == "temperature": - state = self.device.temperature - elif self.type == "voltage": - state = self.device.voltage - elif self.type == "amps": - state = self.device.amps - elif self.type == "watts": - state = self.device.watts - elif self.type == "charge_time": - state = self.device.charge_time - elif self.type == "energy_added": - state = self.device.energy_added - else: - state = "Unknown" - return state + return getattr(self.device, self.entity_description.key, None) diff --git a/homeassistant/components/juicenet/translations/hu.json b/homeassistant/components/juicenet/translations/hu.json index f04a8c1e6ca..63e6086190b 100644 --- a/homeassistant/components/juicenet/translations/hu.json +++ b/homeassistant/components/juicenet/translations/hu.json @@ -13,6 +13,7 @@ "data": { "api_token": "API Token" }, + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre a https://home.juice.net/Manage webhelyen.", "title": "Csatlakoz\u00e1s a JuiceNethez" } } diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 6c82013361a..fbaa730ab9f 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -70,7 +70,7 @@ class KaiterraSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._sensor.get("value") @@ -80,7 +80,7 @@ class KaiterraSensor(SensorEntity): return f"{self._device_id}_{self._kind}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if not self._sensor.get("units"): return None diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 2792246d71c..a1e0387c707 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,9 +1,19 @@ """Support for KEBA charging station sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( +from __future__ import annotations + +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + POWER_KILO_WATT, ) from . import DOMAIN @@ -19,44 +29,55 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensors = [ KebaSensor( keba, - "Curr user", - "Max Current", "max_current", - "mdi:flash", - ELECTRIC_CURRENT_AMPERE, + SensorEntityDescription( + key="Curr user", + name="Max Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), ), KebaSensor( keba, - "Setenergy", - "Energy Target", "energy_target", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="Setenergy", + name="Energy Target", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), ), KebaSensor( keba, - "P", - "Charging Power", "charging_power", - "mdi:flash", - "kW", - DEVICE_CLASS_POWER, + SensorEntityDescription( + key="P", + name="Charging Power", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), ), KebaSensor( keba, - "E pres", - "Session Energy", "session_energy", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="E pres", + name="Session Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), ), KebaSensor( keba, - "E total", - "Total Energy", "total_energy", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="E total", + name="Total Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), ), ] async_add_entities(sensors) @@ -65,53 +86,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaSensor(SensorEntity): """The entity class for KEBA charging stations sensors.""" - def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): + _attr_should_poll = False + + def __init__( + self, + keba, + entity_type, + description: SensorEntityDescription, + ): """Initialize the KEBA Sensor.""" self._keba = keba - self._key = key - self._name = name + self.entity_description = description self._entity_type = entity_type - self._icon = icon - self._unit = unit - self._device_class = device_class - self._state = None - self._attributes = {} + self._attr_name = f"{keba.device_name} {description.name}" + self._attr_unique_id = f"{keba.device_id}_{entity_type}" - @property - def should_poll(self): - """Deactivate polling. Data updated by KebaHandler.""" - return False - - @property - def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_id}_{self._entity_type}" - - @property - def name(self): - """Return the name of the device.""" - return f"{self._keba.device_name} {self._name}" - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Get the unit of measurement.""" - return self._unit + self._attributes: dict[str, str] = {} @property def extra_state_attributes(self): @@ -120,9 +111,9 @@ class KebaSensor(SensorEntity): async def async_update(self): """Get latest cached states from the device.""" - self._state = self._keba.get_value(self._key) + self._attr_native_value = self._keba.get_value(self.entity_description.key) - if self._key == "P": + if self.entity_description.key == "P": self._attributes["power_factor"] = self._keba.get_value("PF") self._attributes["voltage_u1"] = str(self._keba.get_value("U1")) self._attributes["voltage_u2"] = str(self._keba.get_value("U2")) @@ -130,7 +121,7 @@ class KebaSensor(SensorEntity): self._attributes["current_i1"] = str(self._keba.get_value("I1")) self._attributes["current_i2"] = str(self._keba.get_value("I2")) self._attributes["current_i3"] = str(self._keba.get_value("I3")) - elif self._key == "Curr user": + elif self.entity_description.key == "Curr user": self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") def update_callback(self): diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index a6b1b9ada22..b28aac740f1 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -50,7 +50,7 @@ class KiraReceiver(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the receiver.""" return self._state diff --git a/homeassistant/components/kmtronic/translations/zh-Hans.json b/homeassistant/components/kmtronic/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 196448e8262..c1f68e4c376 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -66,11 +66,11 @@ class KNXSensor(KnxEntity, SensorEntity): ) self._attr_force_update = self._device.always_callback self._attr_unique_id = str(self._device.sensor_value.group_address_state) - self._attr_unit_of_measurement = self._device.unit_of_measurement() + self._attr_native_unit_of_measurement = self._device.unit_of_measurement() self._attr_state_class = config.get(CONF_STATE_CLASS) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._device.resolve_state() diff --git a/homeassistant/components/kodi/translations/zh-Hans.json b/homeassistant/components/kodi/translations/zh-Hans.json index 6fe91b6e995..12915ccdb9b 100644 --- a/homeassistant/components/kodi/translations/zh-Hans.json +++ b/homeassistant/components/kodi/translations/zh-Hans.json @@ -1,11 +1,50 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u65e0\u6548\u9a8c\u8bc1", + "no_uuid": "Kodi \u5b9e\u4f8b\u6ca1\u6709\u552f\u4e00\u7684 ID\u3002\u8fd9\u5f88\u53ef\u80fd\u662f\u7531\u4e8e\u65e7\u7684 Kodi \u7248\u672c\uff0817.x \u6216\u66f4\u4f4e\u7248\u672c\uff09\u9020\u6210\u3002\u60a8\u53ef\u4ee5\u624b\u52a8\u914d\u7f6e\u96c6\u6210\u6216\u5347\u7ea7\u5230\u66f4\u65b0\u7684 Kodi \u7248\u672c\u3002", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" - } + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 Kodi \u7528\u6237\u540d\u548c\u5bc6\u7801\u3002\u8fd9\u4e9b\u53ef\u4ee5\u5728\u201c\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\u201d\u4e2d\u627e\u5230\u3002" + }, + "discovery_confirm": { + "description": "\u60a8\u662f\u5426\u60f3\u8981\u5c06 Kodi (`{name}`) \u6dfb\u52a0\u5230 Home Assistant?", + "title": "\u5df2\u53d1\u73b0 Kodi" + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u9a8c\u8bc1" + }, + "description": "Kodi \u8fde\u63a5\u4fe1\u606f\u3002\n\u8bf7\u786e\u4fdd\u5728\u8bbe\u7f6e\uff1a\u201c\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\u201d\u4e2d\u542f\u7528\u201c\u5141\u8bb8\u901a\u8fc7 HTTP \u63a7\u5236 Kodi\u201d\u3002" + }, + "ws_port": { + "data": { + "ws_port": "\u7aef\u53e3" + }, + "description": "WebSocket \u7aef\u53e3(\u5728 Kodi \u4e2d\u6709\u65f6\u79f0\u4e3a TCP \u7aef\u53e3)\u3002\u4e3a\u901a\u8fc7 WebSocket \u8fdb\u884c\u8fde\u63a5\uff0c\u60a8\u9700\u8981\u5728\"\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\"\u4e2d\u542f\u7528\u201c\u5141\u8bb8\u7a0b\u5e8f...\u63a7\u5236 Kodi\u201d\u3002\u5982\u679c\u672a\u542f\u7528 WebSocket\uff0c\u8bf7\u79fb\u9664\u7aef\u53e3\u5e76\u7559\u7a7a\u3002" } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "[entity_name} \u88ab\u8981\u6c42\u5173\u95ed", + "turn_on": "[entity_name} \u88ab\u8981\u6c42\u6253\u5f00" + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 32d0f0e20c0..6785e2e7124 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -36,6 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .config_flow import ( # Loading the config flow file will register the flow CONF_DEFAULT_OPTIONS, @@ -220,7 +221,7 @@ YAML_CONFIGS = "yaml_configs" PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 18975bdb467..a22b30f6862 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -104,12 +104,12 @@ class KonnectedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 507e5d258f2..1ad58223b88 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -3,38 +3,107 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "confirm": { + "description": "Modell: {model}\nAzonos\u00edt\u00f3: {id}\nGazdag\u00e9p: {host}\nPort: {port} \n\n Az IO \u00e9s a panel viselked\u00e9s\u00e9t a Konnected Alarm Panel be\u00e1ll\u00edt\u00e1saiban konfigur\u00e1lhatja.", + "title": "Konnected eszk\u00f6z k\u00e9sz" + }, + "import_confirm": { + "description": "A konfigur\u00e1ci\u00f3s.yaml f\u00e1jlban felfedezt\u00fcnk egy Konnected Alarm Panel-t {id} Ez a folyamat lehet\u0151v\u00e9 teszi, hogy import\u00e1lja azt egy konfigur\u00e1ci\u00f3s bejegyz\u00e9sbe.", + "title": "Konnected eszk\u00f6z import\u00e1l\u00e1sa" + }, "user": { "data": { "host": "IP c\u00edm", "port": "Port" - } + }, + "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel gazdag\u00e9p\u00e9nek adatait." } } }, "options": { + "abort": { + "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z" + }, + "error": { + "bad_host": "\u00c9rv\u00e9nytelen fel\u00fclb\u00edr\u00e1l\u00e1si API host URL", + "one": "\u00dcres", + "other": "\u00dcres" + }, "step": { "options_binary": { "data": { - "name": "N\u00e9v (nem k\u00f6telez\u0151)" - } + "inverse": "Invert\u00e1lja a nyitott/z\u00e1rt \u00e1llapotot", + "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "type": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 t\u00edpusa" + }, + "description": "{zone} opci\u00f3k", + "title": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 konfigur\u00e1l\u00e1sa" }, "options_digital": { "data": { "name": "N\u00e9v (nem k\u00f6telez\u0151)", "poll_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (perc) (opcion\u00e1lis)", "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa" - } + }, + "description": "{zone} opci\u00f3k", + "title": "Digit\u00e1lis \u00e9rz\u00e9kel\u0151 konfigur\u00e1l\u00e1sa" + }, + "options_io": { + "data": { + "1": "1. z\u00f3na", + "2": "2. z\u00f3na", + "3": "3. z\u00f3na", + "4": "4. z\u00f3na", + "5": "5. z\u00f3na", + "6": "6. z\u00f3na", + "7": "7. z\u00f3na", + "out": "KI" + }, + "description": "{model} felfedez\u00e9se {host}-n\u00e1l. V\u00e1lassza ki az al\u00e1bbi I/O alapkonfigur\u00e1ci\u00f3j\u00e1t - az I/O-t\u00f3l f\u00fcgg\u0151en lehet\u0151v\u00e9 teheti bin\u00e1ris \u00e9rz\u00e9kel\u0151k (nyitott/k\u00f6zeli \u00e9rintkez\u0151k), digit\u00e1lis \u00e9rz\u00e9kel\u0151k (dht \u00e9s ds18b20) vagy kapcsolhat\u00f3 kimenetek sz\u00e1m\u00e1ra. A r\u00e9szletes be\u00e1ll\u00edt\u00e1sokat a k\u00f6vetkez\u0151 l\u00e9p\u00e9sekben konfigur\u00e1lhatja.", + "title": "I/O konfigur\u00e1l\u00e1sa" + }, + "options_io_ext": { + "data": { + "10": "10. z\u00f3na", + "11": "11. z\u00f3na", + "12": "12. z\u00f3na", + "8": "8. z\u00f3na", + "9": "9. z\u00f3na", + "alarm1": "RIASZT\u00c1S1", + "alarm2_out2": "KI2/RIASZT\u00c1S2", + "out1": "KI1" + }, + "description": "V\u00e1lassza ki a fennmarad\u00f3 I/O konfigur\u00e1ci\u00f3j\u00e1t al\u00e1bb. A k\u00f6vetkez\u0151 l\u00e9p\u00e9sekben konfigur\u00e1lhatja a r\u00e9szletes be\u00e1ll\u00edt\u00e1sokat.", + "title": "B\u0151v\u00edtett I/O konfigur\u00e1l\u00e1sa" + }, + "options_misc": { + "data": { + "api_host": "API host URL fel\u00fclb\u00edr\u00e1l\u00e1sa (opcion\u00e1lis)", + "blink": "A panel LED villog\u00e1sa \u00e1llapotv\u00e1ltoz\u00e1skor", + "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", + "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API gazdag\u00e9p-URL fel\u00fcl\u00edr\u00e1sa" + }, + "description": "K\u00e9rj\u00fck, v\u00e1lassza ki a k\u00edv\u00e1nt viselked\u00e9st a panelhez", + "title": "Egy\u00e9b be\u00e1ll\u00edt\u00e1sa" }, "options_switch": { "data": { - "name": "N\u00e9v (nem k\u00f6telez\u0151)" - } + "activation": "Kimenet bekapcsolt \u00e1llapotban", + "momentary": "Impulzus id\u0151tartama (ms) (opcion\u00e1lis)", + "more_states": "Tov\u00e1bbi \u00e1llapotok konfigur\u00e1l\u00e1sa ehhez a z\u00f3n\u00e1hoz", + "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "pause": "Sz\u00fcnet impulzusok k\u00f6z\u00f6tt (ms) (opcion\u00e1lis)", + "repeat": "Ism\u00e9tl\u00e9si id\u0151k (-1 = v\u00e9gtelen) (opcion\u00e1lis)" + }, + "description": "{zone} opci\u00f3k: \u00e1llapot {state}", + "title": "Kapcsolhat\u00f3 kimenet konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 5c223f4f5d6..9f902da7d2f 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,5 +1,10 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -40,6 +45,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -51,6 +57,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -65,28 +72,44 @@ SENSOR_PROCESS_DATA = [ "devices:local", "HomeGrid_P", "Home Power from Grid", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "HomeOwn_P", "Home Power from Own", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "HomePv_P", "Home Power from PV", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "Home_P", "Home Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -97,6 +120,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -104,28 +128,44 @@ SENSOR_PROCESS_DATA = [ "devices:local:pv1", "P", "DC1 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local:pv2", "P", "DC2 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local:pv3", "P", "DC3 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "PV2Bat_P", "PV to Battery Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -139,14 +179,18 @@ SENSOR_PROCESS_DATA = [ "devices:local:battery", "Cycles", "Battery Cycles", - {ATTR_ICON: "mdi:recycle"}, + {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT}, "format_round", ), ( "devices:local:battery", "P", "Battery Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -174,7 +218,11 @@ SENSOR_PROCESS_DATA = [ "scb:statistic:EnergyFlow", "Statistic:Autarky:Total", "Autarky Total", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -202,7 +250,11 @@ SENSOR_PROCESS_DATA = [ "scb:statistic:EnergyFlow", "Statistic:OwnConsumptionRate:Total", "Own Consumption Rate Total", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -249,6 +301,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -289,6 +342,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -329,6 +383,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -369,6 +424,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -409,6 +465,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -449,6 +506,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -489,6 +547,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -530,6 +589,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 717dfacbfdf..19ac4db0f90 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Any, Callable -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -165,7 +165,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): return f"{self.platform_name} {self._sensor_name}" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of this Sensor Entity or None.""" return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) @@ -179,13 +179,18 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return self._sensor_data.get(ATTR_DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the class of the state of this device, from component STATE_CLASSES.""" + return self._sensor_data.get(ATTR_STATE_CLASS) + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state of the sensor.""" if self.coordinator.data is None: # None is translated to STATE_UNKNOWN diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hans.json b/homeassistant/components/kostal_plenticore/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index bc0a0a21845..1b9f8ca13cc 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -124,7 +124,7 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): return self._name.lower() @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return self._state @@ -229,7 +229,7 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): return "mdi:cash" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if "number_of" not in self._sensor_type: return self._unit_of_measurement diff --git a/homeassistant/components/kraken/translations/es-419.json b/homeassistant/components/kraken/translations/es-419.json new file mode 100644 index 00000000000..106ff98de0d --- /dev/null +++ b/homeassistant/components/kraken/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "one": "", + "other": "Otros" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json index afcf3f92d45..1befa14a52b 100644 --- a/homeassistant/components/kraken/translations/es.json +++ b/homeassistant/components/kraken/translations/es.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "one": "", + "other": "Otros" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json index 7de89d6b2dc..25fe63bebd5 100644 --- a/homeassistant/components/kraken/translations/nl.json +++ b/homeassistant/components/kraken/translations/nl.json @@ -9,10 +9,6 @@ }, "step": { "user": { - "data": { - "one": "Leeg", - "other": "Leeg" - }, "description": "Wil je beginnen met instellen?" } } diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 1b56803fae6..c6d2794a06d 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -94,13 +94,13 @@ class KWBSensor(SensorEntity): return self._sensor.available @property - def state(self): + def native_value(self): """Return the state of value.""" if self._sensor.value is not None and self._sensor.available: return self._sensor.value return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._sensor.unit_of_measurement diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 2f93196a4bb..99aa39ce7cd 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -176,10 +176,10 @@ class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._temperature @@ -187,11 +187,11 @@ class LaCrosseTemperature(LaCrosseSensor): class LaCrosseHumidity(LaCrosseSensor): """Implementation of a Lacrosse humidity sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_icon = "mdi:water-percent" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._humidity @@ -200,7 +200,7 @@ class LaCrosseBattery(LaCrosseSensor): """Implementation of a Lacrosse battery sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._low_battery is None: return None diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 128450826d6..66f05c5d34d 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -77,7 +77,7 @@ class LastfmSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 831e44dca8f..18a947f7757 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -42,48 +42,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LaunchLibrarySensor(SensorEntity): """Representation of a launch_library Sensor.""" - def __init__(self, launches: PyLaunches, name: str) -> None: + _attr_icon = "mdi:rocket" + + def __init__(self, api: PyLaunches, name: str) -> None: """Initialize the sensor.""" - self.launches = launches - self.next_launch = None - self._name = name + self.api = api + self._attr_name = name async def async_update(self) -> None: """Get the latest data.""" try: - launches = await self.launches.upcoming_launches() + launches = await self.api.upcoming_launches() except PyLaunchesException as exception: _LOGGER.error("Error getting data, %s", exception) + self._attr_available = False else: - if launches: - self.next_launch = launches[0] - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def state(self) -> str | None: - """Return the state of the sensor.""" - if self.next_launch: - return self.next_launch.name - return None - - @property - def icon(self) -> str: - """Return the icon of the sensor.""" - return "mdi:rocket" - - @property - def extra_state_attributes(self) -> dict | None: - """Return attributes for the sensor.""" - if self.next_launch: - return { - ATTR_LAUNCH_TIME: self.next_launch.net, - ATTR_AGENCY: self.next_launch.launch_service_provider.name, - ATTR_AGENCY_COUNTRY_CODE: self.next_launch.pad.location.country_code, - ATTR_STREAM: self.next_launch.webcast_live, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - return None + if next_launch := next((launch for launch in launches), None): + self._attr_available = True + self._attr_native_value = next_launch.name + self._attr_extra_state_attributes = { + ATTR_LAUNCH_TIME: next_launch.net, + ATTR_AGENCY: next_launch.launch_service_provider.name, + ATTR_AGENCY_COUNTRY_CODE: next_launch.pad.location.country_code, + ATTR_STREAM: next_launch.webcast_live, + ATTR_ATTRIBUTION: ATTRIBUTION, + } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index fdd6ee51872..965e9626f66 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -93,12 +93,12 @@ class LcnVariableSensor(LcnEntity, SensorEntity): await self.device_connection.cancel_status_request_handler(self.variable) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self._value @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return cast(str, self.unit.value) @@ -145,7 +145,7 @@ class LcnLedLogicSensor(LcnEntity, SensorEntity): await self.device_connection.cancel_status_request_handler(self.source) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self._value diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 603efee6d9d..5dbd2898971 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -18,7 +18,9 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd a [Life360 dokument\u00e1ci\u00f3]({docs_url}) c\u00edm\u0171 r\u00e9szt.\n \u00c9rdemes ezt megtenni a fi\u00f3kok hozz\u00e1ad\u00e1sa el\u0151tt.", + "title": "Life360 fi\u00f3kadatok" } } } diff --git a/homeassistant/components/light/translations/hu.json b/homeassistant/components/light/translations/hu.json index ad215a5ba4c..1ac835fd1af 100644 --- a/homeassistant/components/light/translations/hu.json +++ b/homeassistant/components/light/translations/hu.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "{entity_name} f\u00e9nyerej\u00e9nek cs\u00f6kkent\u00e9se", "brightness_increase": "{entity_name} f\u00e9nyerej\u00e9nek n\u00f6vel\u00e9se", + "flash": "Vaku {entity_name}", "toggle": "{entity_name} fel/lekapcsol\u00e1sa", "turn_off": "{entity_name} lekapcsol\u00e1sa", "turn_on": "{entity_name} felkapcsol\u00e1sa" diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index b298b78c7f6..369256ce403 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -26,7 +26,7 @@ class LightwaveBattery(SensorEntity): """Lightwave TRV Battery.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, name, lwlink, serial): @@ -43,7 +43,7 @@ class LightwaveBattery(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index b7746392cee..18f1c81e368 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -90,12 +90,12 @@ class LinuxBatterySensor(SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._battery_stat.capacity @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PERCENTAGE diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index 32d39e995e1..41875da9e69 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -15,5 +15,15 @@ "title": "Conectarse a LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transici\u00f3n predeterminada (segundos)" + }, + "title": "Configurar LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/hu.json b/homeassistant/components/litejet/translations/hu.json index 3ee53c086bf..910d34cdc1a 100644 --- a/homeassistant/components/litejet/translations/hu.json +++ b/homeassistant/components/litejet/translations/hu.json @@ -15,5 +15,15 @@ "title": "Csatlakoz\u00e1s a LiteJet-hez" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Alap\u00e9rtelmezett \u00e1tmenet (m\u00e1sodperc)" + }, + "title": "A LiteJet konfigur\u00e1l\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/no.json b/homeassistant/components/litejet/translations/no.json index d3206ca2897..f6e2379900d 100644 --- a/homeassistant/components/litejet/translations/no.json +++ b/homeassistant/components/litejet/translations/no.json @@ -15,5 +15,15 @@ "title": "Koble til LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standard overgang (sekunder)" + }, + "title": "Konfigurer LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/zh-Hans.json b/homeassistant/components/litejet/translations/zh-Hans.json new file mode 100644 index 00000000000..133385be2d3 --- /dev/null +++ b/homeassistant/components/litejet/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "\u914d\u7f6e LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 15ea68f8342..cbcb75c0b23 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -36,7 +36,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): self.sensor_attribute = sensor_attribute @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" return getattr(self.robot, self.sensor_attribute) @@ -45,7 +45,7 @@ class LitterRobotWasteSensor(LitterRobotPropertySensor): """Litter-Robot waste sensor.""" @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @@ -59,10 +59,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state.""" if self.robot.sleep_mode_enabled: - return super().state.isoformat() + return super().native_value.isoformat() return None @property diff --git a/homeassistant/components/litterrobot/translations/zh-Hans.json b/homeassistant/components/litterrobot/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 86a075c1a14..6e665ccd1c2 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -1,4 +1,6 @@ """Camera that loads a picture from a local file.""" +from __future__ import annotations + import logging import mimetypes import os @@ -73,7 +75,9 @@ class LocalFile(Camera): if content is not None: self.content_type = content - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" try: with open(self._file_path, "rb") as file: @@ -84,6 +88,7 @@ class LocalFile(Camera): self._name, self._file_path, ) + return None def check_file_path_access(self, file_path): """Check that filepath given is readable.""" diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index 661ef88e641..bd1b3d54fac 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -33,6 +33,6 @@ class IPSensor(SensorEntity): async def async_update(self) -> None: """Fetch new state data for the sensor.""" - self._attr_state = await async_get_source_ip( + self._attr_native_value = await async_get_source_ip( self.hass, target_ip=PUBLIC_TARGET_IP ) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 9e1a4803e11..d9060b10080 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -29,8 +29,8 @@ from .const import ( DEFAULT_CACHEDB, DOMAIN, LED_MODE_KEY, - LOGI_SENSORS, RECORDING_MODE_KEY, + SENSOR_TYPES, SIGNAL_LOGI_CIRCLE_RECONFIGURE, SIGNAL_LOGI_CIRCLE_RECORD, SIGNAL_LOGI_CIRCLE_SNAPSHOT, @@ -50,10 +50,12 @@ ATTR_DURATION = "duration" PLATFORMS = ["camera", "sensor"] +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] + SENSOR_SCHEMA = vol.Schema( { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(LOGI_SENSORS)): vol.All( - cv.ensure_list, [vol.In(LOGI_SENSORS)] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ) } ) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 1afeb190c8b..30407f03ecf 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -1,4 +1,6 @@ """Support to the Logi Circle cameras.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -142,7 +144,9 @@ class LogiCam(Camera): return state - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image from the camera.""" return await self._camera.live_stream.download_jpeg() diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index 92967d2eb84..02e51993198 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,4 +1,7 @@ """Constants in Logi Circle component.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import PERCENTAGE DOMAIN = "logi_circle" @@ -12,15 +15,40 @@ DEFAULT_CACHEDB = ".logi_cache.pickle" LED_MODE_KEY = "LED" RECORDING_MODE_KEY = "RECORDING_MODE" -# Sensor types: Name, unit of measure, icon per sensor key. -LOGI_SENSORS = { - "battery_level": ["Battery", PERCENTAGE, "battery-50"], - "last_activity_time": ["Last Activity", None, "history"], - "recording": ["Recording Mode", None, "eye"], - "signal_strength_category": ["WiFi Signal Category", None, "wifi"], - "signal_strength_percentage": ["WiFi Signal Strength", PERCENTAGE, "wifi"], - "streaming": ["Streaming Mode", None, "camera"], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="battery_level", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:battery-50", + ), + SensorEntityDescription( + key="last_activity_time", + name="Last Activity", + icon="mdi:history", + ), + SensorEntityDescription( + key="recording", + name="Recording Mode", + icon="mdi:eye", + ), + SensorEntityDescription( + key="signal_strength_category", + name="WiFi Signal Category", + icon="mdi:wifi", + ), + SensorEntityDescription( + key="signal_strength_percentage", + name="WiFi Signal Strength", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:wifi", + ), + SensorEntityDescription( + key="streaming", + name="Streaming Mode", + icon="mdi:camera", + ), +) SIGNAL_LOGI_CIRCLE_RECONFIGURE = "logi_circle_reconfigure" SIGNAL_LOGI_CIRCLE_SNAPSHOT = "logi_circle_snapshot" diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 29cd6e28e1c..50671152587 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -1,7 +1,10 @@ """Support for Logi Circle sensors.""" -import logging +from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +import logging +from typing import Any + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -13,12 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.dt import as_local -from .const import ( - ATTRIBUTION, - DEVICE_BRAND, - DOMAIN as LOGI_CIRCLE_DOMAIN, - LOGI_SENSORS as SENSOR_TYPES, -) +from .const import ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -33,44 +31,30 @@ async def async_setup_entry(hass, entry, async_add_entities): devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras time_zone = str(hass.config.time_zone) - sensors = [] - for sensor_type in entry.data.get(CONF_SENSORS).get(CONF_MONITORED_CONDITIONS): - for device in devices: - if device.supports_feature(sensor_type): - sensors.append(LogiSensor(device, time_zone, sensor_type)) + monitored_conditions = entry.data.get(CONF_SENSORS).get(CONF_MONITORED_CONDITIONS) + entities = [ + LogiSensor(device, time_zone, description) + for description in SENSOR_TYPES + if description.key in monitored_conditions + for device in devices + if device.supports_feature(description.key) + ] - async_add_entities(sensors, True) + async_add_entities(entities, True) class LogiSensor(SensorEntity): """A sensor implementation for a Logi Circle camera.""" - def __init__(self, camera, time_zone, sensor_type): + def __init__(self, camera, time_zone, description: SensorEntityDescription): """Initialize a sensor for Logi Circle camera.""" - self._sensor_type = sensor_type + self.entity_description = description self._camera = camera - self._id = f"{self._camera.mac_address}-{self._sensor_type}" - self._icon = f"mdi:{SENSOR_TYPES.get(self._sensor_type)[2]}" - self._name = f"{self._camera.name} {SENSOR_TYPES.get(self._sensor_type)[0]}" - self._activity = {} - self._state = None + self._attr_unique_id = f"{camera.mac_address}-{description.key}" + self._attr_name = f"{camera.name} {description.name}" + self._activity: dict[Any, Any] = {} self._tz = time_zone - @property - def unique_id(self): - """Return a unique ID.""" - return self._id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - @property def device_info(self): """Return information about the device.""" @@ -93,7 +77,7 @@ class LogiSensor(SensorEntity): "microphone_gain": self._camera.microphone_gain, } - if self._sensor_type == "battery_level": + if self.entity_description.key == "battery_level": state[ATTR_BATTERY_CHARGING] = self._camera.charging return state @@ -101,37 +85,36 @@ class LogiSensor(SensorEntity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery_level" and self._state is not None: + sensor_type = self.entity_description.key + if sensor_type == "battery_level" and self._attr_native_value is not None: return icon_for_battery_level( - battery_level=int(self._state), charging=False + battery_level=int(self._attr_native_value), charging=False ) - if self._sensor_type == "recording_mode" and self._state is not None: - return "mdi:eye" if self._state == STATE_ON else "mdi:eye-off" - if self._sensor_type == "streaming_mode" and self._state is not None: - return "mdi:camera" if self._state == STATE_ON else "mdi:camera-off" - return self._icon - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] + if sensor_type == "recording_mode" and self._attr_native_value is not None: + return "mdi:eye" if self._attr_native_value == STATE_ON else "mdi:eye-off" + if sensor_type == "streaming_mode" and self._attr_native_value is not None: + return ( + "mdi:camera" + if self._attr_native_value == STATE_ON + else "mdi:camera-off" + ) + return self.entity_description.icon async def async_update(self): """Get the latest data and updates the state.""" - _LOGGER.debug("Pulling data from %s sensor", self._name) + _LOGGER.debug("Pulling data from %s sensor", self.name) await self._camera.update() - if self._sensor_type == "last_activity_time": + if self.entity_description.key == "last_activity_time": last_activity = await self._camera.get_last_activity(force_refresh=True) if last_activity is not None: last_activity_time = as_local(last_activity.end_time_utc) - self._state = ( + self._attr_native_value = ( f"{last_activity_time.hour:0>2}:{last_activity_time.minute:0>2}" ) else: - state = getattr(self._camera, self._sensor_type, None) + state = getattr(self._camera, self.entity_description.key, None) if isinstance(state, bool): - self._state = STATE_ON if state is True else STATE_OFF + self._attr_native_value = STATE_ON if state is True else STATE_OFF else: - self._state = state - self._state = state + self._attr_native_value = state diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 9c788350de4..73522a59519 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "external_error": "Kiv\u00e9tel t\u00f6rt\u00e9nt egy m\u00e1sik folyamatb\u00f3l.", + "external_setup": "LogiCircle sikeresen konfigur\u00e1lva egy m\u00e1sik folyamatb\u00f3l.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, "error": { @@ -10,10 +12,15 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "auth": { + "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot. \n\n [Link] ({authorization_url})", + "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" + }, "user": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, + "description": "V\u00e1lassza ki, melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n kereszt\u00fcl szeretn\u00e9 hiteles\u00edteni a LogiCircle szolg\u00e1ltat\u00e1st.", "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" } } diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 23eea5c00e0..23bc67f46bc 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -108,7 +108,7 @@ class AirSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index b7f2cb50cbf..eb962772fe5 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -67,7 +67,7 @@ class LondonTubeSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 78e55f22eb8..05d7f79ebfd 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -97,7 +97,7 @@ class LoopEnergySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -107,7 +107,7 @@ class LoopEnergySensor(SensorEntity): return False @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e16f1399c40..d8fe591a0ba 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Lovelace commands.""" mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index b27cc35ab26..b4bdd7f30b3 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -73,7 +73,7 @@ class LuftdatenSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._data is not None: try: @@ -82,7 +82,7 @@ class LuftdatenSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 905fc05bf8e..0e8960530e3 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -10,6 +10,10 @@ }, "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a bridge-t ({host}) a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lva.", + "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." + }, "link": { "description": "A(z) {name} {host} p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", "title": "P\u00e1ros\u00edtsd a h\u00edddal" diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 39cfff38a1b..84e3744a0e2 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -103,12 +103,12 @@ class LyftSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e958567940a..4afb66f7173 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -50,7 +51,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Honeywell Lyric component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index f4d4d4b999a..868b6262ddc 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -94,7 +94,7 @@ class LyricSensor(LyricDeviceEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -123,7 +123,7 @@ class LyricIndoorTemperatureSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.indoorTemperature @@ -152,7 +152,7 @@ class LyricOutdoorTemperatureSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.outdoorTemperature @@ -181,7 +181,7 @@ class LyricOutdoorHumiditySensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.displayedOutdoorHumidity @@ -209,7 +209,7 @@ class LyricNextPeriodSensor(LyricSensor): ) @property - def state(self) -> datetime: + def native_value(self) -> datetime: """Return the state of the sensor.""" device = self.device time = dt_util.parse_time(device.changeableValues.nextPeriodTime) @@ -242,7 +242,7 @@ class LyricSetpointStatusSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" device = self.device if device.changeableValues.thermostatSetpointStatus == PRESET_HOLD_UNTIL: diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 0dd27a60ae0..12288c5ab78 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -115,7 +115,7 @@ class MagicSeaweedSensor(SensorEntity): return f"{self.hour} {self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -125,7 +125,7 @@ class MagicSeaweedSensor(SensorEntity): return self._unit_system @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index 66988def22d..c58a27c3370 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -21,4 +21,4 @@ send_message: description: Extended information of notification. Supports list of images. Optional. example: "{'images': ['/tmp/test.jpg']}" selector: - text: + object: diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 673c965544b..03bfbd23b31 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -46,7 +46,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): return f"{self.vin}_fuel_remaining_percentage" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @@ -56,7 +56,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): return "mdi:gas-station" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.data["status"]["fuelRemainingPercent"] @@ -76,7 +76,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): return f"{self.vin}_fuel_distance_remaining" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: return LENGTH_MILES @@ -88,7 +88,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): return "mdi:gas-station" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" fuel_distance_km = self.data["status"]["fuelDistanceRemainingKm"] return ( @@ -115,7 +115,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity): return f"{self.vin}_odometer" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: return LENGTH_MILES @@ -127,7 +127,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity): return "mdi:speedometer" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" odometer_km = self.data["status"]["odometerKm"] return ( @@ -152,7 +152,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_front_left_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -162,7 +162,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["frontLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -183,7 +183,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_front_right_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -193,7 +193,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["frontRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -214,7 +214,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_rear_left_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -224,7 +224,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["rearLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -245,7 +245,7 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_rear_right_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -255,7 +255,7 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["rearRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 5b027a99bf9..cb485ac765f 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -14,6 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import local_source, models @@ -36,7 +37,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str: return uri -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media_source component.""" hass.data[DOMAIN] = {} hass.components.websocket_api.async_register_command(websocket_browse_media) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 12b80554933..69efa26ac44 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from .const import DOMAIN @@ -44,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigEntry): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Establish connection with MELCloud.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 6c303e8e3c3..608c3547724 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -11,11 +11,11 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS -from homeassistant.util import dt as dt_util from . import MelCloudDevice from .const import DOMAIN @@ -41,7 +41,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="room_temperature", name="Room Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.room_temperature, enabled=lambda x: True, @@ -50,7 +50,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="energy", name="Energy", icon="mdi:factory", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, @@ -61,7 +61,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="outside_temperature", name="Outside Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.outside_temperature, enabled=lambda x: True, @@ -70,7 +70,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="tank_temperature", name="Tank Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, @@ -81,7 +81,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="room_temperature", name="Room Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.room_temperature, enabled=lambda x: True, @@ -90,7 +90,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="flow_temperature", name="Flow Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.flow_temperature, enabled=lambda x: True, @@ -99,7 +99,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="return_temperature", name="Flow Return Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.return_temperature, enabled=lambda x: True, @@ -150,13 +150,14 @@ class MelDeviceSensor(SensorEntity): self._attr_name = f"{api.name} {description.name}" self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" - self._attr_state_class = STATE_CLASS_MEASUREMENT if description.device_class == DEVICE_CLASS_ENERGY: - self._attr_last_reset = dt_util.utc_from_timestamp(0) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + else: + self._attr_state_class = STATE_CLASS_MEASUREMENT @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.entity_description.value_fn(self._api) @@ -187,6 +188,6 @@ class AtwZoneSensor(MelDeviceSensor): self._attr_name = f"{api.name} {zone.name} {description.name}" @property - def state(self): + def native_value(self): """Return zone based state.""" return self.entity_description.value_fn(self._zone) diff --git a/homeassistant/components/melcloud/translations/hu.json b/homeassistant/components/melcloud/translations/hu.json index 7f81269c700..5744b71c780 100644 --- a/homeassistant/components/melcloud/translations/hu.json +++ b/homeassistant/components/melcloud/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A MELCloud integr\u00e1ci\u00f3 m\u00e1r be van \u00e1ll\u00edtva ehhez az e-mailhez. A hozz\u00e1f\u00e9r\u00e9si token friss\u00edtve lett." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", @@ -10,7 +13,9 @@ "data": { "password": "Jelsz\u00f3", "username": "E-mail" - } + }, + "description": "Csatlakozzon a MELCloud-fi\u00f3kj\u00e1val.", + "title": "Csatlakozzon a MELCloudhoz" } } } 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/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index ed1978d160d..df006c78194 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -109,7 +109,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): } @property - def state(self): + def native_value(self): """Return the state.""" path = SENSOR_TYPES[self._type][ENTITY_API_DATA_PATH].split(":") data = getattr(self.coordinator.data, path[0]) @@ -135,7 +135,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): return value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_TYPES[self._type][ENTITY_UNIT] @@ -164,7 +164,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): """Representation of a Meteo-France rain sensor.""" @property - def state(self): + def native_value(self): """Return the state.""" # search first cadran with rain next_rain = next( @@ -202,7 +202,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): self._unique_id = self._name @property - def state(self): + def native_value(self): """Return the state.""" return get_warning_text_status_from_indice_color( self.coordinator.data.get_domain_max_color() diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 101b889498d..b5a07ad06e6 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -51,7 +51,9 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): f"{station.name} {SENSOR_TYPES[sensor_type][SENSOR_TYPE_NAME]}" ) self._attr_unique_id = f"{station.code}_{sensor_type}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_UNIT) + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type].get( + SENSOR_TYPE_UNIT + ) @property def device_info(self): @@ -65,7 +67,7 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): } @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( getattr(self.coordinator.data["weather"], self._type) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 749282b1a21..4919e36bd58 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -42,7 +42,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="name", name="Station Name", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:label-outline", entity_registry_enabled_default=False, ), @@ -50,7 +50,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="weather", name="Weather", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), @@ -58,7 +58,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=True, ), @@ -66,7 +66,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="feels_like_temperature", name="Feels Like Temperature", device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=False, ), @@ -74,7 +74,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_speed", name="Wind Speed", device_class=None, - unit_of_measurement=SPEED_MILES_PER_HOUR, + native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", entity_registry_enabled_default=True, ), @@ -82,7 +82,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_direction", name="Wind Direction", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), @@ -90,7 +90,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_gust", name="Wind Gust", device_class=None, - unit_of_measurement=SPEED_MILES_PER_HOUR, + native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", entity_registry_enabled_default=False, ), @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:eye", entity_registry_enabled_default=False, ), @@ -106,7 +106,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility_distance", name="Visibility Distance", device_class=None, - unit_of_measurement=LENGTH_KILOMETERS, + native_unit_of_measurement=LENGTH_KILOMETERS, icon="mdi:eye", entity_registry_enabled_default=False, ), @@ -114,7 +114,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="uv", name="UV Index", device_class=None, - unit_of_measurement=UV_INDEX, + native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", entity_registry_enabled_default=True, ), @@ -122,7 +122,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="precipitation", name="Probability of Precipitation", device_class=None, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", entity_registry_enabled_default=True, ), @@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="humidity", name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, entity_registry_enabled_default=False, ), @@ -189,7 +189,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self.use_3hourly = use_3hourly @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = None diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index fafaf53ff99..b27f719d974 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -88,7 +88,7 @@ class MfiSensor(SensorEntity): return self._port.label @property - def state(self): + def native_value(self): """Return the state of the sensor.""" try: tag = self._port.tag @@ -115,7 +115,7 @@ class MfiSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" try: tag = self._port.tag diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 63a1181f720..7d5d5eba183 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -90,12 +90,12 @@ class MHZ19Sensor(SensorEntity): return f"{self._name}: {SENSOR_TYPES[self._sensor_type][0]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._ppm if self._sensor_type == SENSOR_CO2 else self._temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index a7aab41bea9..f712ffe6fe5 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -180,7 +180,7 @@ class MiFloraSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -207,7 +207,7 @@ class MiFloraSensor(SensorEntity): return STATE_CLASS_MEASUREMENT @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit diff --git a/homeassistant/components/mikrotik/translations/zh-Hans.json b/homeassistant/components/mikrotik/translations/zh-Hans.json index 9604af53495..14916be1264 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hans.json +++ b/homeassistant/components/mikrotik/translations/zh-Hans.json @@ -1,14 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { "host": "\u4e3b\u673a", - "name": "\u540d\u5b57", + "name": "\u540d\u79f0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d", - "verify_ssl": "\u4f7f\u7528 ssl" + "verify_ssl": "\u4f7f\u7528 SSL" + }, + "title": "\u8bbe\u7f6e Mikrotik \u8def\u7531\u5668" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u542f\u7528 ARP Ping", + "force_dhcp": "\u4f7f\u7528 DHCP \u5f3a\u5236\u626b\u63cf" } } } diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 75422cd26e1..73cb65daf05 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,15 +1,43 @@ """The mill component.""" +from datetime import timedelta +import logging + from mill import Mill from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + PLATFORMS = ["climate", "sensor"] +class MillDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Mill data.""" + + def __init__( + self, + hass: HomeAssistant, + *, + mill_data_connection: Mill, + ) -> None: + """Initialize global Mill data updater.""" + self.mill_data_connection = mill_data_connection + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_method=mill_data_connection.fetch_heater_data, + update_interval=timedelta(seconds=30), + ) + + async def async_setup_entry(hass, entry): """Set up the Mill heater.""" mill_data_connection = Mill( @@ -20,9 +48,12 @@ async def async_setup_entry(hass, entry): if not await mill_data_connection.connect(): raise ConfigEntryNotReady - await mill_data_connection.find_all_heaters() + hass.data[DOMAIN] = MillDataUpdateCoordinator( + hass, + mill_data_connection=mill_data_connection, + ) - hass.data[DOMAIN] = mill_data_connection + await hass.data[DOMAIN].async_config_entry_first_refresh() hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 16c78329b0b..199bdf393a1 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -11,8 +11,10 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_AWAY_TEMP, @@ -41,11 +43,11 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] dev = [] - for heater in mill_data_connection.heaters.values(): - dev.append(MillHeater(heater, mill_data_connection)) + for heater in mill_data_coordinator.data.values(): + dev.append(MillHeater(mill_data_coordinator, heater)) async_add_entities(dev) async def set_room_temp(service): @@ -54,7 +56,7 @@ async def async_setup_entry(hass, entry, async_add_entities): sleep_temp = service.data.get(ATTR_SLEEP_TEMP) comfort_temp = service.data.get(ATTR_COMFORT_TEMP) away_temp = service.data.get(ATTR_AWAY_TEMP) - await mill_data_connection.set_room_temperatures_by_name( + await mill_data_coordinator.mill_data_connection.set_room_temperatures_by_name( room_name, sleep_temp, comfort_temp, away_temp ) @@ -63,122 +65,97 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class MillHeater(ClimateEntity): +class MillHeater(CoordinatorEntity, ClimateEntity): """Representation of a Mill Thermostat device.""" _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_supported_features = SUPPORT_FLAGS - _attr_target_temperature_step = 1 + _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, heater, mill_data_connection): + def __init__(self, coordinator, heater): """Initialize the thermostat.""" - self._heater = heater - self._conn = mill_data_connection + super().__init__(coordinator) + + self._id = heater.device_id self._attr_unique_id = heater.device_id self._attr_name = heater.name - - @property - def available(self): - """Return True if entity is available.""" - return self._heater.available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - res = { - "open_window": self._heater.open_window, - "heating": self._heater.is_heating, - "controlled_by_tibber": self._heater.tibber_control, - "heater_generation": 1 if self._heater.is_gen1 else 2, + self._attr_device_info = { + "identifiers": {(DOMAIN, heater.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._heater.room: - res["room"] = self._heater.room.name - res["avg_room_temp"] = self._heater.room.avg_temp + if heater.is_gen1: + self._attr_hvac_modes = [HVAC_MODE_HEAT] else: - res["room"] = "Independent device" - return res - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._heater.set_temp - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._heater.current_temp - - @property - def fan_mode(self): - """Return the fan setting.""" - return FAN_ON if self._heater.fan_status == 1 else HVAC_MODE_OFF - - @property - def hvac_action(self): - """Return current hvac i.e. heat, cool, idle.""" - if self._heater.is_gen1 or self._heater.is_heating == 1: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._heater.is_gen1 or self._heater.power_status == 1: - return HVAC_MODE_HEAT - return HVAC_MODE_OFF - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - if self._heater.is_gen1: - return [HVAC_MODE_HEAT] - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._update_attr(heater) async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self._conn.set_heater_temp(self._heater.device_id, int(temperature)) + await self.coordinator.mill_data_connection.set_heater_temp( + self._id, int(temperature) + ) + await self.coordinator.async_request_refresh() async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" fan_status = 1 if fan_mode == FAN_ON else 0 - await self._conn.heater_control(self._heater.device_id, fan_status=fan_status) + await self.coordinator.mill_data_connection.heater_control( + self._id, fan_status=fan_status + ) + await self.coordinator.async_request_refresh() async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + heater = self.coordinator.data[self._id] + if hvac_mode == HVAC_MODE_HEAT: - await self._conn.heater_control(self._heater.device_id, power_status=1) - elif hvac_mode == HVAC_MODE_OFF and not self._heater.is_gen1: - await self._conn.heater_control(self._heater.device_id, power_status=0) + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=1 + ) + await self.coordinator.async_request_refresh() + elif hvac_mode == HVAC_MODE_OFF and not heater.is_gen1: + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=0 + ) + await self.coordinator.async_request_refresh() - async def async_update(self): - """Retrieve latest state.""" - self._heater = await self._conn.update_device(self._heater.device_id) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self._heater.device_id - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": f"generation {1 if self._heater.is_gen1 else 2}", + @callback + def _update_attr(self, heater): + self._attr_available = heater.available + self._attr_extra_state_attributes = { + "open_window": heater.open_window, + "heating": heater.is_heating, + "controlled_by_tibber": heater.tibber_control, + "heater_generation": 1 if heater.is_gen1 else 2, } - return device_info + if heater.room: + self._attr_extra_state_attributes["room"] = heater.room.name + self._attr_extra_state_attributes["avg_room_temp"] = heater.room.avg_temp + else: + self._attr_extra_state_attributes["room"] = "Independent device" + self._attr_target_temperature = heater.set_temp + self._attr_current_temperature = heater.current_temp + self._attr_fan_mode = FAN_ON if heater.fan_status == 1 else HVAC_MODE_OFF + if heater.is_gen1 or heater.is_heating == 1: + self._attr_hvac_action = CURRENT_HVAC_HEAT + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE + if heater.is_gen1 or heater.power_status == 1: + self._attr_hvac_mode = HVAC_MODE_HEAT + else: + self._attr_hvac_mode = HVAC_MODE_OFF diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 161bbe274ef..33a7c35c169 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.5.0"], + "requirements": ["millheater==0.5.2"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 8b68d0ebe38..11b006e4b6e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -2,11 +2,12 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) -from homeassistant.const import ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN -from homeassistant.util import dt as dt_util +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -14,72 +15,51 @@ from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] - dev = [] - for heater in mill_data_connection.heaters.values(): - for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR): - dev.append( - MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) - ) - async_add_entities(dev) + entities = [ + MillHeaterEnergySensor(mill_data_coordinator, sensor_type, heater) + for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR) + for heater in mill_data_coordinator.data.values() + ] + async_add_entities(entities) -class MillHeaterEnergySensor(SensorEntity): +class MillHeaterEnergySensor(CoordinatorEntity, SensorEntity): """Representation of a Mill Sensor device.""" - def __init__(self, heater, mill_data_connection, sensor_type): + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_state_class = STATE_CLASS_TOTAL_INCREASING + + def __init__(self, coordinator, sensor_type, heater): """Initialize the sensor.""" + super().__init__(coordinator) + self._id = heater.device_id - self._conn = mill_data_connection self._sensor_type = sensor_type - self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" self._attr_unique_id = f"{heater.device_id}_{sensor_type}" - self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = { "identifiers": {(DOMAIN, heater.device_id)}, "name": self.name, "manufacturer": MANUFACTURER, "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) + self._update_attr(heater) - async def async_update(self): - """Retrieve latest state.""" - heater = await self._conn.update_device(self._id) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() + + @callback + def _update_attr(self, heater): self._attr_available = heater.available if self._sensor_type == CONSUMPTION_TODAY: - _state = heater.day_consumption + self._attr_native_value = heater.day_consumption elif self._sensor_type == CONSUMPTION_YEAR: - _state = heater.year_consumption - else: - _state = None - if _state is None: - self._attr_state = _state - return - - if self.state not in [STATE_UNKNOWN, None] and _state < self.state: - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) - self._attr_state = _state + self._attr_native_value = heater.year_consumption diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index d103ff8eaa6..e4b4cdf9922 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -172,7 +172,7 @@ class MinMaxSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._unit_of_measurement_mismatch: return None @@ -181,7 +181,7 @@ class MinMaxSensor(SensorEntity): ) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._unit_of_measurement_mismatch: return "ERR" diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 651c2762c55..9f1c89f09c6 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -70,12 +70,12 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): return self._server.online @property - def state(self) -> Any: + def native_value(self) -> Any: """Return sensor state.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return sensor measurement unit.""" return self._unit diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 247c1ffc1c3..ef3c228d2d5 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,7 +4,9 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa." + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", + "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC -c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_port": "A portnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { @@ -12,6 +14,7 @@ "host": "Hoszt", "name": "N\u00e9v" }, + "description": "\u00c1ll\u00edtsa be a Minecraft Server p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a megfigyel\u00e9st.", "title": "Kapcsold \u00f6ssze a Minecraft szervered" } } diff --git a/homeassistant/components/minecraft_server/translations/zh-Hans.json b/homeassistant/components/minecraft_server/translations/zh-Hans.json new file mode 100644 index 00000000000..ef3c08c8434 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002\u8bf7\u68c0\u67e5\u4e3b\u673a\u5730\u5740\u548c\u7aef\u53e3\u5e76\u91cd\u8bd5\uff0c\u4e14\u786e\u4fdd\u60a8\u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884c\u7684 Minecraft \u7248\u672c\u81f3\u5c11\u5728 1.7 \u4ee5\u4e0a\u3002", + "invalid_ip": "IP \u5730\u5740\u65e0\u6548 (\u65e0\u6cd5\u786e\u5b9a MAC \u5730\u5740)\u3002\u8bf7\u66f4\u6b63\u5e76\u91cd\u8bd5\u3002", + "invalid_port": "\u7aef\u53e3\u7684\u8303\u56f4\u5728 1024 \u5230 65535 \u4e4b\u95f4\u3002\u8bf7\u66f4\u6b63\u5e76\u91cd\u8bd5\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 Minecraft \u670d\u52a1\u5668\u5b9e\u4f8b\u4ee5\u5141\u8bb8\u76d1\u63a7\u3002", + "title": "\u8fde\u63a5\u60a8\u7684 Minecraft \u670d\u52a1\u5668" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 670a6daf3d3..ed6c7f27b94 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,12 +1,19 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" +from __future__ import annotations + import logging +from typing import Any import btlewrap from btlewrap.base import BluetoothBackendException from mitemp_bt import mitemp_bt_poller import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MAC, @@ -44,18 +51,34 @@ DEFAULT_RETRIES = 2 DEFAULT_TIMEOUT = 10 -# Sensor types are defined like: Name, units -SENSOR_TYPES = { - "temperature": [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], - "humidity": [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], - "battery": [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="battery", + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + native_unit_of_measurement=PERCENTAGE, + ), +) + +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, @@ -73,79 +96,46 @@ def setup_platform(hass, config, add_entities, discovery_info=None): backend = BACKEND _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) - cache = config.get(CONF_CACHE) + cache = config[CONF_CACHE] poller = mitemp_bt_poller.MiTempBtPoller( - config.get(CONF_MAC), + config[CONF_MAC], cache_timeout=cache, - adapter=config.get(CONF_ADAPTER), + adapter=config[CONF_ADAPTER], backend=backend, ) - force_update = config.get(CONF_FORCE_UPDATE) - median = config.get(CONF_MEDIAN) - poller.ble_timeout = config.get(CONF_TIMEOUT) - poller.retries = config.get(CONF_RETRIES) + prefix = config[CONF_NAME] + force_update = config[CONF_FORCE_UPDATE] + median = config[CONF_MEDIAN] + poller.ble_timeout = config[CONF_TIMEOUT] + poller.retries = config[CONF_RETRIES] - devs = [] + monitored_conditions = config[CONF_MONITORED_CONDITIONS] + entities = [ + MiTempBtSensor(poller, prefix, force_update, median, description) + for description in SENSOR_TYPES + if description.key in monitored_conditions + ] - for parameter in config[CONF_MONITORED_CONDITIONS]: - device = SENSOR_TYPES[parameter][0] - name = SENSOR_TYPES[parameter][1] - unit = SENSOR_TYPES[parameter][2] - - prefix = config.get(CONF_NAME) - if prefix: - name = f"{prefix} {name}" - - devs.append( - MiTempBtSensor(poller, parameter, device, name, unit, force_update, median) - ) - - add_entities(devs) + add_entities(entities) class MiTempBtSensor(SensorEntity): """Implementing the MiTempBt sensor.""" - def __init__(self, poller, parameter, device, name, unit, force_update, median): + def __init__( + self, poller, prefix, force_update, median, description: SensorEntityDescription + ): """Initialize the sensor.""" + self.entity_description = description self.poller = poller - self.parameter = parameter - self._device = device - self._unit = unit - self._name = name - self._state = None - self.data = [] - self._force_update = force_update + self.data: list[Any] = [] + self._attr_name = f"{prefix} {description.name}" + self._attr_force_update = force_update # Median is used to filter out outliers. median of 3 will filter # single outliers, while median of 5 will filter double outliers # Use median_count = 1 if no filtering is required. self.median_count = median - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return self._unit - - @property - def device_class(self): - """Device class of this entity.""" - return self._device - - @property - def force_update(self): - """Force update.""" - return self._force_update - def update(self): """ Update current conditions. @@ -154,7 +144,7 @@ class MiTempBtSensor(SensorEntity): """ try: _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.parameter) + data = self.poller.parameter_value(self.entity_description.key) except OSError as ioerr: _LOGGER.warning("Polling error %s", ioerr) return @@ -174,7 +164,7 @@ class MiTempBtSensor(SensorEntity): if self.data: self.data = self.data[1:] else: - self._state = None + self._attr_native_value = None return if len(self.data) > self.median_count: @@ -183,6 +173,6 @@ class MiTempBtSensor(SensorEntity): if len(self.data) == self.median_count: median = sorted(self.data)[int((self.median_count - 1) / 2)] _LOGGER.debug("Median is: %s", median) - self._state = median + self._attr_native_value = median else: _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index d5008d1778c..d486f78d334 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,4 +1,6 @@ """Support for IP Cameras.""" +from __future__ import annotations + import asyncio from contextlib import closing import logging @@ -106,7 +108,9 @@ class MjpegCamera(Camera): self._auth = aiohttp.BasicAuth(self._username, password=self._password) self._verify_ssl = device_info.get(CONF_VERIFY_SSL) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" # DigestAuth is not supported if ( @@ -130,11 +134,17 @@ class MjpegCamera(Camera): except aiohttp.ClientError as err: _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) - def camera_image(self): + return None + + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" if self._username and self._password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: - auth = HTTPDigestAuth(self._username, self._password) + auth: HTTPDigestAuth | HTTPBasicAuth = HTTPDigestAuth( + self._username, self._password + ) else: auth = HTTPBasicAuth(self._username, self._password) req = requests.get( diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 9633ec6556d..1fc5be2a890 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -36,7 +36,7 @@ from .webhook import handle_webhook PLATFORMS = "sensor", "binary_sensor", "device_tracker" -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 7e3c1c13148..f6652f7f889 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -74,11 +74,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): """Representation of an mobile app sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._config[ATTR_SENSOR_STATE] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 16be39230db..e98a61257c6 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -42,6 +42,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from .const import ( @@ -66,14 +67,13 @@ from .const import ( CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_MSG_WAIT, CONF_PARITY, CONF_PRECISION, CONF_RETRIES, CONF_RETRY_ON_EMPTY, CONF_REVERSE_ORDER, - CONF_RTUOVERTCP, CONF_SCALE, - CONF_SERIAL, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -90,8 +90,6 @@ from .const import ( CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, - CONF_TCP, - CONF_UDP, CONF_VERIFY, CONF_WRITE_TYPE, DATA_TYPE_CUSTOM, @@ -112,9 +110,18 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, + RTUOVERTCP, + SERIAL, + TCP, + UDP, +) +from .modbus import ModbusHub, async_modbus_setup +from .validators import ( + duplicate_entity_validator, + number_validator, + scan_interval_validator, + struct_validator, ) -from .modbus import async_modbus_setup -from .validators import number_validator, scan_interval_validator, struct_validator _LOGGER = logging.getLogger(__name__) @@ -283,6 +290,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] ), @@ -301,7 +309,7 @@ MODBUS_SCHEMA = vol.Schema( SERIAL_SCHEMA = MODBUS_SCHEMA.extend( { - vol.Required(CONF_TYPE): CONF_SERIAL, + vol.Required(CONF_TYPE): SERIAL, vol.Required(CONF_BAUDRATE): cv.positive_int, vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8), vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"), @@ -315,7 +323,7 @@ ETHERNET_SCHEMA = MODBUS_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TYPE): vol.Any(CONF_TCP, CONF_UDP, CONF_RTUOVERTCP), + vol.Required(CONF_TYPE): vol.Any(TCP, UDP, RTUOVERTCP), } ) @@ -324,6 +332,7 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.All( cv.ensure_list, scan_interval_validator, + duplicate_entity_validator, [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], @@ -355,6 +364,11 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( ) +def get_hub(hass: HomeAssistant, name: str) -> ModbusHub: + """Return modbus hub with name.""" + return hass.data[DOMAIN][name] + + async def async_setup(hass, config): """Set up Modbus component.""" return await async_modbus_setup( diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index f767201496c..468e61aefa8 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_STRUCTURE, STATE_ON, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity @@ -124,48 +124,46 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers = self._swap_registers(registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DATA_TYPE_STRING: - self._value = byte_string.decode() - else: - val = struct.unpack(self._structure, byte_string) + return byte_string.decode() - # Issue: https://github.com/home-assistant/core/issues/41944 - # If unpack() returns a tuple greater than 1, don't try to process the value. - # Instead, return the values of unpack(...) separated by commas. - if len(val) > 1: - # Apply scale and precision to floats and ints - v_result = [] - for entry in val: - v_temp = self._scale * entry + self._offset - - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(v_temp, int) and self._precision == 0: - v_result.append(str(v_temp)) - else: - v_result.append(f"{float(v_temp):.{self._precision}f}") - self._value = ",".join(map(str, v_result)) - else: - # Apply scale and precision to floats and ints - val = self._scale * val[0] + self._offset + val = struct.unpack(self._structure, byte_string) + # Issue: https://github.com/home-assistant/core/issues/41944 + # If unpack() returns a tuple greater than 1, don't try to process the value. + # Instead, return the values of unpack(...) separated by commas. + if len(val) > 1: + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset # We could convert int to float, and the code would still work; however # we lose some precision, and unit tests will fail. Therefore, we do # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) else: - self._value = f"{float(val):.{self._precision}f}" + v_result.append(f"{float(v_temp):.{self._precision}f}") + return ",".join(map(str, v_result)) + + # Apply scale and precision to floats and ints + val = self._scale * val[0] + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: + return str(val) + return f"{float(val):.{self._precision}f}" -class BaseSwitch(BasePlatform, RestoreEntity): +class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): """Base class representing a Modbus switch.""" def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" config[CONF_INPUT_TYPE] = "" super().__init__(hub, config) - self._is_on = None + self._attr_is_on = False convert = { CALL_TYPE_REGISTER_HOLDING: ( CALL_TYPE_REGISTER_HOLDING, @@ -189,9 +187,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: @@ -202,12 +200,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._is_on = state.state == STATE_ON - - @property - def is_on(self): - """Return true if switch is on.""" - return self._is_on + self._attr_is_on = state.state == STATE_ON async def async_turn(self, command): """Evaluate switch result.""" @@ -221,7 +214,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._attr_available = True if not self._verify_active: - self._is_on = command == self.command_on + self._attr_is_on = command == self.command_on self.async_write_ha_state() return @@ -258,13 +251,13 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._attr_available = True if self._verify_type == CALL_TYPE_COIL: - self._is_on = bool(result.bits[0] & 1) + self._attr_is_on = bool(result.bits[0] & 1) else: value = int(result.registers[0]) if value == self._state_on: - self._is_on = True + self._attr_is_on = True elif value == self._state_off: - self._is_on = False + self._attr_is_on = False elif value is not None: _LOGGER.error( "Unexpected response from modbus device slave %s register %s, got 0x%2x", diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index ac635c76275..08ebfc72880 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -9,8 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BasePlatform -from .const import MODBUS_DOMAIN PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_BINARY_SENSORS]: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = get_hub(hass, discovery_info[CONF_NAME]) sensors.append(ModbusBinarySensor(hub, entry)) async_add_entities(sensors) @@ -43,14 +43,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._value = state.state == STATE_ON - else: - self._value = None - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._value + self._attr_is_on = state.state == STATE_ON async def async_update(self, now=None): """Update the state of the sensor.""" @@ -68,6 +61,6 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): self.async_write_ha_state() return - self._value = result.bits[0] & 1 + self._attr_is_on = result.bits[0] & 1 self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 16334d883a9..831f3c979cc 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BaseStructPlatform from .const import ( ATTR_TEMPERATURE, @@ -39,7 +40,6 @@ from .const import ( DATA_TYPE_UINT16, DATA_TYPE_UINT32, DATA_TYPE_UINT64, - MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -59,7 +59,7 @@ async def async_setup_platform( entities = [] for entity in discovery_info[CONF_CLIMATES]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) entities.append(ModbusThermostat(hub, entity)) async_add_entities(entities) @@ -165,8 +165,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_available = False return -1 - self.unpack_structure_result(result.registers) - + self._value = self.unpack_structure_result(result.registers) self._attr_available = True if self._value is None: diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 10eb07f801e..01e0fdd5e13 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -30,6 +30,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" @@ -38,9 +39,7 @@ CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" -CONF_RTUOVERTCP = "rtuovertcp" CONF_SCALE = "scale" -CONF_SERIAL = "serial" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OFF = "state_off" @@ -57,13 +56,16 @@ CONF_SWAP_NONE = "none" CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" CONF_TARGET_TEMP = "target_temp_register" -CONF_TCP = "tcp" -CONF_UDP = "udp" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" CONF_WRITE_TYPE = "write_type" +RTUOVERTCP = "rtuovertcp" +SERIAL = "serial" +TCP = "tcp" +UDP = "udp" + # service call attributes ATTR_ADDRESS = "address" ATTR_HUB = "hub" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 98a352f218a..64165412d27 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, @@ -30,7 +31,6 @@ from .const import ( CONF_STATE_OPENING, CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, - MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -50,7 +50,7 @@ async def async_setup_platform( covers = [] for cover in discovery_info[CONF_COVERS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) covers.append(ModbusCover(hub, cover)) async_add_entities(covers) @@ -109,22 +109,13 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): STATE_UNAVAILABLE: None, STATE_UNKNOWN: None, } - self._value = convert[state.state] + self._set_attr_state(convert[state.state]) - @property - def is_opening(self): - """Return if the cover is opening or not.""" - return self._value == self._state_opening - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - return self._value == self._state_closing - - @property - def is_closed(self): - """Return if the cover is closed or not.""" - return self._value == self._state_closed + def _set_attr_state(self, value): + """Convert received value to HA state.""" + self._attr_is_opening = value == self._state_opening + self._attr_is_closing = value == self._state_closing + self._attr_is_closed = value == self._state_closed async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" @@ -160,7 +151,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): return None self._attr_available = True if self._input_type == CALL_TYPE_COIL: - self._value = bool(result.bits[0] & 1) + self._set_attr_state(bool(result.bits[0] & 1)) else: - self._value = int(result.registers[0]) + self._set_attr_state(int(result.registers[0])) self.async_write_ha_state() diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index a4d4265846d..cf5c9762db8 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -8,8 +8,9 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import CONF_FANS, MODBUS_DOMAIN +from .const import CONF_FANS from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -25,7 +26,7 @@ async def async_setup_platform( fans = [] for entry in discovery_info[CONF_FANS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) fans.append(ModbusFan(hub, entry)) async_add_entities(fans) @@ -42,3 +43,11 @@ class ModbusFan(BaseSwitch, FanEntity): ) -> None: """Set fan on.""" await self.async_turn(self.command_on) + + @property + def is_on(self): + """Return true if fan is on. + + This is needed due to the ongoing conversion of fan. + """ + return self._attr_is_on diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index 3eae5ed3db3..dd9a8ad754d 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_LIGHTS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -25,7 +25,7 @@ async def async_setup_platform( lights = [] for entry in discovery_info[CONF_LIGHTS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) lights.append(ModbusLight(hub, entry)) async_add_entities(lights) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 9f2208de175..549ad2c2351 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.2"], + "requirements": ["pymodbus==2.5.3rc1"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "silver", "iot_class": "local_polling" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 77d8b669c24..7cab51f7fe6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,4 +1,6 @@ """Support for Modbus.""" +from __future__ import annotations + import asyncio from collections import namedtuple import logging @@ -39,23 +41,25 @@ from .const import ( CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLOSE_COMM_ON_ERROR, + CONF_MSG_WAIT, CONF_PARITY, CONF_RETRIES, CONF_RETRY_ON_EMPTY, - CONF_RTUOVERTCP, - CONF_SERIAL, CONF_STOPBITS, - CONF_TCP, - CONF_UDP, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, PLATFORMS, + RTUOVERTCP, + SERIAL, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, + TCP, + UDP, ) _LOGGER = logging.getLogger(__name__) + ConfEntry = namedtuple("ConfEntry", "call_type attr func_name") RunEntry = namedtuple("RunEntry", "attr func") PYMODBUS_CALL = [ @@ -183,6 +187,8 @@ async def async_modbus_setup( class ModbusHub: """Thread safe wrapper class for pymodbus.""" + name: str + def __init__(self, hass, client_config): """Initialize the Modbus hub.""" @@ -192,15 +198,15 @@ class ModbusHub: self._in_error = False self._lock = asyncio.Lock() self.hass = hass - self._config_name = client_config[CONF_NAME] + self.name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_delay = client_config[CONF_DELAY] self._pb_call = {} self._pb_class = { - CONF_SERIAL: ModbusSerialClient, - CONF_TCP: ModbusTcpClient, - CONF_UDP: ModbusUdpClient, - CONF_RTUOVERTCP: ModbusTcpClient, + SERIAL: ModbusSerialClient, + TCP: ModbusTcpClient, + UDP: ModbusUdpClient, + RTUOVERTCP: ModbusTcpClient, } self._pb_params = { "port": client_config[CONF_PORT], @@ -209,7 +215,7 @@ class ModbusHub: "retries": client_config[CONF_RETRIES], "retry_on_empty": client_config[CONF_RETRY_ON_EMPTY], } - if self._config_type == CONF_SERIAL: + if self._config_type == SERIAL: # serial configuration self._pb_params.update( { @@ -223,10 +229,16 @@ class ModbusHub: else: # network configuration self._pb_params["host"] = client_config[CONF_HOST] - if self._config_type == CONF_RTUOVERTCP: + if self._config_type == RTUOVERTCP: 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 == 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}" @@ -255,7 +267,7 @@ class ModbusHub: """Try to connect, and retry if needed.""" async with self._lock: if not await self.hass.async_add_executor_job(self._pymodbus_connect): - err = f"{self._config_name} connect failed, retry in pymodbus" + err = f"{self.name} connect failed, retry in pymodbus" self._log_error(err, error_state=False) return @@ -271,8 +283,11 @@ class ModbusHub: self._async_cancel_listener = None self._config_delay = 0 - def _pymodbus_close(self): - """Close sync. pymodbus.""" + async def async_close(self): + """Disconnect client.""" + if self._async_cancel_listener: + self._async_cancel_listener() + self._async_cancel_listener = None if self._client: try: self._client.close() @@ -280,15 +295,6 @@ class ModbusHub: self._log_error(str(exception_error)) self._client = None - async def async_close(self): - """Disconnect client.""" - if self._async_cancel_listener: - self._async_cancel_listener() - self._async_cancel_listener = None - - async with self._lock: - return await self.hass.async_add_executor_job(self._pymodbus_close) - def _pymodbus_connect(self): """Connect client.""" try: @@ -322,7 +328,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/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index e969fa23a65..3165f416a6e 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -10,8 +10,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BaseStructPlatform -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -31,7 +31,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_SENSORS]: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = get_hub(hass, discovery_info[CONF_NAME]) sensors.append(ModbusRegisterSensor(hub, entry)) async_add_entities(sensors) @@ -47,19 +47,14 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._attr_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) async def async_added_to_hass(self): """Handle entity which will be added.""" await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._value = state.state - - @property - def state(self): - """Return the state of the sensor.""" - return self._value + self._attr_native_value = state.state async def async_update(self, now=None): """Update the state of the sensor.""" @@ -73,6 +68,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() return - self.unpack_structure_result(result.registers) + self._attr_native_value = self.unpack_structure_result(result.registers) self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 820e43419a0..55dc014420f 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_NAME, CONF_SWITCHES from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -26,7 +26,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_SWITCHES]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) switches.append(ModbusSwitch(hub, entry)) async_add_entities(switches) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 3efb61f8027..543618e11fd 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -9,9 +9,11 @@ from typing import Any import voluptuous as vol from homeassistant.const import ( + CONF_ADDRESS, CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, + CONF_SLAVE, CONF_STRUCTURE, CONF_TIMEOUT, ) @@ -86,6 +88,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 @@ -188,3 +191,35 @@ def scan_interval_validator(config: dict) -> dict: ) hub[CONF_TIMEOUT] = minimum_scan_interval - 1 return config + + +def duplicate_entity_validator(config: dict) -> dict: + """Control scan_interval.""" + for hub_index, hub in enumerate(config): + addresses: set[str] = set() + for component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + names: set[str] = set() + errors: list[int] = [] + for index, entry in enumerate(hub[conf_key]): + name = entry[CONF_NAME] + addr = str(entry[CONF_ADDRESS]) + if CONF_SLAVE in entry: + addr += "_" + str(entry[CONF_SLAVE]) + if addr in addresses: + err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + elif name in names: + err = f"Modbus {component}/{name}  is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + else: + names.add(name) + addresses.add(addr) + + for i in reversed(errors): + del config[hub_index][conf_key][i] + + return config diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 080e077a457..afbc09eb45c 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -75,7 +75,7 @@ class ModemCalleridSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 01efe3f1d28..1e51ec9a1ae 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -73,7 +73,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.light_sleep_timer @@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.fan_sleep_timer diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 7bfa161f9ec..c57903ce5b7 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -359,12 +359,12 @@ class MoldIndicator(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/monoprice/translations/hu.json b/homeassistant/components/monoprice/translations/hu.json index a845f862160..fd11a8fbc0f 100644 --- a/homeassistant/components/monoprice/translations/hu.json +++ b/homeassistant/components/monoprice/translations/hu.json @@ -10,8 +10,30 @@ "step": { "user": { "data": { - "port": "Port" - } + "port": "Port", + "source_1": "Forr\u00e1s neve #1", + "source_2": "Forr\u00e1s neve #2", + "source_3": "Forr\u00e1s neve #3", + "source_4": "Forr\u00e1s neve #4", + "source_5": "Forr\u00e1s neve #5", + "source_6": "Forr\u00e1s neve #6" + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Forr\u00e1s neve #1", + "source_2": "Forr\u00e1s neve #2", + "source_3": "Forr\u00e1s neve #3", + "source_4": "Forr\u00e1s neve #4", + "source_5": "Forr\u00e1s neve #5", + "source_6": "Forr\u00e1s neve #6" + }, + "title": "Forr\u00e1sok konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 6213e218d24..223ee831779 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -60,7 +60,7 @@ class MoonSensor(SensorEntity): return "moon__phase" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._state == 0: return STATE_NEW_MOON diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index be88a099f25..9c6db5d88ec 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -47,7 +47,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """ _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, blind): """Initialize the Motion Battery Sensor.""" @@ -70,7 +70,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._blind.battery_level @@ -106,7 +106,7 @@ class MotionTDBUBatterySensor(MotionBatterySensor): self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._blind.battery_level is None: return None @@ -128,7 +128,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH _attr_entity_registry_enabled_default = False - _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT def __init__(self, coordinator, device, device_type): """Initialize the Motion Signal Strength Sensor.""" @@ -162,7 +162,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.RSSI diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 19f0c70c4d6..a2560e5fa79 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -8,24 +8,29 @@ "error": { "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t" }, + "flow_title": "Mozg\u00f3 red\u0151ny", "step": { "connect": { "data": { "api_key": "API kulcs" }, - "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" + "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Mozg\u00f3 red\u0151ny" }, "select": { "data": { "select_ip": "IP c\u00edm" - } + }, + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha tov\u00e1bbi Motion Gateway-eket szeretne csatlakoztatni", + "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Motion Gateway-t" }, "user": { "data": { "api_key": "API kulcs", "host": "IP c\u00edm" }, - "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja" + "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "title": "Mozg\u00f3 red\u0151ny" } } } diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 2ade7c48e1b..3eebcd4ee53 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,30 +237,34 @@ 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, - ): - hass.async_create_task(client.async_set_camera(camera_id, camera)) + if url: + set_motion_event = _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_storage_event = _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 set_motion_event or set_storage_event: + hass.async_create_task(client.async_set_camera(camera_id, camera)) async_dispatcher_send( hass, diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index adcb9ca623a..ebd6956e8fd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,4 +1,6 @@ """Camera that loads a picture from an MQTT topic.""" +from __future__ import annotations + import functools import voluptuous as vol @@ -98,6 +100,8 @@ class MqttCamera(MqttEntity, Camera): }, ) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" return self._last_image diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f4629499db0..c5441840878 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -53,18 +53,23 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False -PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } -).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +PLATFORM_SCHEMA = vol.All( + # Deprecated, remove in Home Assistant 2021.11 + cv.deprecated(CONF_LAST_RESET_TOPIC), + cv.deprecated(CONF_LAST_RESET_VALUE_TEMPLATE), + mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), +) async def async_setup_platform( @@ -214,7 +219,7 @@ class MqttSensor(MqttEntity, SensorEntity): self.async_write_ha_state() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) @@ -224,7 +229,7 @@ class MqttSensor(MqttEntity, SensorEntity): return self._config[CONF_FORCE_UPDATE] @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 84c4a40f082..a519cab55d3 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "\u00c9rv\u00e9nytelen sz\u00fclet\u00e9si t\u00e9ma.", + "bad_will": "\u00c9rv\u00e9nytelen t\u00e9ma.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { @@ -59,9 +61,25 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", + "title": "Br\u00f3ker opci\u00f3k" }, "options": { + "data": { + "birth_enable": "Sz\u00fclet\u00e9si \u00fczenet enged\u00e9lyez\u00e9se", + "birth_payload": "Sz\u00fclet\u00e9si \u00fczenet", + "birth_qos": "Sz\u00fclet\u00e9si \u00fczenet QoS", + "birth_retain": "A sz\u00fclet\u00e9si \u00fczenet meg\u0151rz\u00e9se", + "birth_topic": "Sz\u00fclet\u00e9si \u00fczenet t\u00e9m\u00e1ja", + "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se", + "will_enable": "Enged\u00e9lyez\u00e9si \u00fczenet", + "will_payload": "\u00dczenet", + "will_qos": "QoS \u00fczenet", + "will_retain": "\u00dczenet megtart\u00e1sa", + "will_topic": "\u00dczenet t\u00e9m\u00e1ja" + }, + "description": "Felfedez\u00e9s - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), a Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nSz\u00fclet\u00e9si \u00fczenet - A sz\u00fclet\u00e9si \u00fczenetet minden alkalommal elk\u00fcldi, amikor a Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nAkarat \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor a Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind takar\u00edt\u00e1s eset\u00e9n (pl. A Home Assistant le\u00e1ll\u00edt\u00e1sa), mind tiszt\u00e1talans\u00e1g eset\u00e9n (pl. Home Assistant \u00f6sszeomlik vagy megszakad a h\u00e1l\u00f3zati kapcsolata) bontani.", "title": "MQTT opci\u00f3k" } } diff --git a/homeassistant/components/mqtt/translations/lt.json b/homeassistant/components/mqtt/translations/lt.json new file mode 100644 index 00000000000..35257770c75 --- /dev/null +++ b/homeassistant/components/mqtt/translations/lt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepavyko prisijungti" + }, + "step": { + "broker": { + "data": { + "password": "Slapta\u017eodis", + "port": "Portas", + "username": "Prisijungimo vardas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index b40d550abf6..479b02ebcbd 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -139,7 +139,7 @@ class MQTTRoomSensor(SensorEntity): return {ATTR_DISTANCE: self._distance} @property - def state(self): + def native_value(self): """Return the current room of the entity.""" return self._state diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index 613cac29b1c..dccab9e8d1e 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktivieredie Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 953fe4c69a8..416ce21cbaf 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -108,7 +108,7 @@ class MVGLiveSensor(SensorEntity): return self._station @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state @@ -128,7 +128,7 @@ class MVGLiveSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index 18b5e95d838..1a5613d8864 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -98,7 +98,7 @@ class MyChevyStatus(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -166,7 +166,7 @@ class EVSensor(SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -176,7 +176,7 @@ class EVSensor(SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement the state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 063f044117e..253c10544c9 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -3,6 +3,13 @@ from datetime import timedelta import logging import pymyq +from pymyq.const import ( + DEVICE_STATE as MYQ_DEVICE_STATE, + DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, + KNOWN_MODELS, + MANUFACTURER, +) +from pymyq.device import MyQDevice from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant.config_entries import ConfigEntry @@ -10,7 +17,11 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL @@ -63,3 +74,46 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class MyQEntity(CoordinatorEntity): + """Base class for MyQ Entities.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device: MyQDevice) -> None: + """Initialize class.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = device.device_id + + @property + def name(self): + """Return the name if any, name can change if user changes it within MyQ.""" + return self._device.name + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "manufacturer": MANUFACTURER, + "sw_version": self._device.firmware_version, + } + model = ( + KNOWN_MODELS.get(self._device.device_id[2:4]) + if self._device.device_id is not None + else None + ) + if model: + device_info["model"] = model + if self._device.parent_device_id: + device_info["via_device"] = (DOMAIN, self._device.parent_device_id) + return device_info + + @property + def available(self): + """Return if the device is online.""" + # Not all devices report online so assume True if its missing + return super().available and self._device.device_json[MYQ_DEVICE_STATE].get( + MYQ_DEVICE_STATE_ONLINE, True + ) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 96ab589253b..9f2d766fcc4 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,17 +1,10 @@ """Support for MyQ gateways.""" -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - KNOWN_MODELS, - MANUFACTURER, -) - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY @@ -29,16 +22,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): +class MyQBinarySensorEntity(MyQEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" _attr_device_class = DEVICE_CLASS_CONNECTIVITY - def __init__(self, coordinator, device): - """Initialize with API object, device id.""" - super().__init__(coordinator) - self._device = device - @property def name(self): """Return the name of the garage door if any.""" @@ -47,35 +35,9 @@ class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): @property def is_on(self): """Return if the device is online.""" - if not self.coordinator.last_update_success: - return False - - # Not all devices report online so assume True if its missing - return self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) + return super().available @property def available(self) -> bool: """Entity is always available.""" return True - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._device.device_id - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } - model = KNOWN_MODELS.get(self._device.device_id[2:4]) - if model: - device_info["model"] = model - - return device_info 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/const.py b/homeassistant/components/myq/const.py index 6189b1601ea..9f3a434ae37 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -5,18 +5,28 @@ from pymyq.garagedoor import ( STATE_OPEN as MYQ_COVER_STATE_OPEN, STATE_OPENING as MYQ_COVER_STATE_OPENING, ) +from pymyq.lamp import STATE_OFF as MYQ_LIGHT_STATE_OFF, STATE_ON as MYQ_LIGHT_STATE_ON -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.const import ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_OPENING, +) DOMAIN = "myq" -PLATFORMS = ["cover", "binary_sensor"] +PLATFORMS = ["cover", "binary_sensor", "light"] MYQ_TO_HASS = { MYQ_COVER_STATE_CLOSED: STATE_CLOSED, MYQ_COVER_STATE_CLOSING: STATE_CLOSING, MYQ_COVER_STATE_OPEN: STATE_OPEN, MYQ_COVER_STATE_OPENING: STATE_OPENING, + MYQ_LIGHT_STATE_ON: STATE_ON, + MYQ_LIGHT_STATE_OFF: STATE_OFF, } MYQ_GATEWAY = "myq_gateway" diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 3d587635f2d..e8e06dc3b22 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,13 +1,7 @@ """Support for MyQ-Enabled Garage Doors.""" import logging -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE, - KNOWN_MODELS, - MANUFACTURER, -) +from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE from pymyq.errors import MyQError from homeassistant.components.cover import ( @@ -18,8 +12,9 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.exceptions import HomeAssistantError +from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS _LOGGER = logging.getLogger(__name__) @@ -32,41 +27,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = data[MYQ_COORDINATOR] async_add_entities( - [MyQDevice(coordinator, device) for device in myq.covers.values()] + [MyQCover(coordinator, device) for device in myq.covers.values()] ) -class MyQDevice(CoordinatorEntity, CoverEntity): +class MyQCover(MyQEntity, CoverEntity): """Representation of a MyQ cover.""" + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + def __init__(self, coordinator, device): """Initialize with API object, device id.""" - super().__init__(coordinator) + super().__init__(coordinator, device) self._device = device - - @property - def device_class(self): - """Define this cover as a garage door.""" - device_type = self._device.device_type - if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: - return DEVICE_CLASS_GATE - return DEVICE_CLASS_GARAGE - - @property - def name(self): - """Return the name of the garage door if any.""" - return self._device.name - - @property - def available(self): - """Return if the device is online.""" - if not self.coordinator.last_update_success: - return False - - # Not all devices report online so assume True if its missing - return self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) + if device.device_type == MYQ_DEVICE_TYPE_GATE: + self._attr_device_class = DEVICE_CLASS_GATE + else: + self._attr_device_class = DEVICE_CLASS_GARAGE + self._attr_unique_id = device.device_id @property def is_closed(self): @@ -88,16 +66,6 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Return if the cover is opening or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._device.device_id - async def async_close_cover(self, **kwargs): """Issue close command to cover.""" if self.is_closing or self.is_closed: @@ -106,21 +74,21 @@ class MyQDevice(CoordinatorEntity, CoverEntity): try: wait_task = await self._device.close(wait_for_state=False) except MyQError as err: - _LOGGER.error( - "Closing of cover %s failed with error: %s", self._device.name, str(err) - ) - - return + raise HomeAssistantError( + f"Closing of cover {self._device.name} failed with error: {err}" + ) from err # Write closing state to HASS self.async_write_ha_state() - if not await wait_task: - _LOGGER.error("Closing of cover %s failed", self._device.name) + result = wait_task if isinstance(wait_task, bool) else await wait_task # Write final state to HASS self.async_write_ha_state() + if not result: + raise HomeAssistantError(f"Closing of cover {self._device.name} failed") + async def async_open_cover(self, **kwargs): """Issue open command to cover.""" if self.is_opening or self.is_open: @@ -129,32 +97,17 @@ class MyQDevice(CoordinatorEntity, CoverEntity): try: wait_task = await self._device.open(wait_for_state=False) except MyQError as err: - _LOGGER.error( - "Opening of cover %s failed with error: %s", self._device.name, str(err) - ) - return + raise HomeAssistantError( + f"Opening of cover {self._device.name} failed with error: {err}" + ) from err # Write opening state to HASS self.async_write_ha_state() - if not await wait_task: - _LOGGER.error("Opening of cover %s failed", self._device.name) + result = wait_task if isinstance(wait_task, bool) else await wait_task # Write final state to HASS self.async_write_ha_state() - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } - model = KNOWN_MODELS.get(self._device.device_id[2:4]) - if model: - device_info["model"] = model - if self._device.parent_device_id: - device_info["via_device"] = (DOMAIN, self._device.parent_device_id) - return device_info + if not result: + raise HomeAssistantError(f"Opening of cover {self._device.name} failed") diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py new file mode 100644 index 00000000000..d8154d7c427 --- /dev/null +++ b/homeassistant/components/myq/light.py @@ -0,0 +1,70 @@ +"""Support for MyQ-Enabled lights.""" +import logging + +from pymyq.errors import MyQError + +from homeassistant.components.light import LightEntity +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.exceptions import HomeAssistantError + +from . import MyQEntity +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up myq lights.""" + data = hass.data[DOMAIN][config_entry.entry_id] + myq = data[MYQ_GATEWAY] + coordinator = data[MYQ_COORDINATOR] + + async_add_entities( + [MyQLight(coordinator, device) for device in myq.lamps.values()], True + ) + + +class MyQLight(MyQEntity, LightEntity): + """Representation of a MyQ light.""" + + _attr_supported_features = 0 + + @property + def is_on(self): + """Return true if the light is on, else False.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_ON + + @property + def is_off(self): + """Return true if the light is off, else False.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_OFF + + async def async_turn_on(self, **kwargs): + """Issue on command to light.""" + if self.is_on: + return + + try: + await self._device.turnon(wait_for_state=True) + except MyQError as err: + raise HomeAssistantError( + f"Turning light {self._device.name} on failed with error: {err}" + ) from err + + # Write new state to HASS + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Issue off command to light.""" + if self.is_off: + return + + try: + await self._device.turnoff(wait_for_state=True) + except MyQError as err: + raise HomeAssistantError( + f"Turning light {self._device.name} off failed with error: {err}" + ) from err + + # Write new state to HASS + self.async_write_ha_state() diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index a93501c941f..33cbea71bcd 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,8 +2,8 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==3.0.4"], - "codeowners": ["@bdraco"], + "requirements": ["pymyq==3.1.2"], + "codeowners": ["@bdraco","@ehendrix23"], "config_flow": true, "homekit": { "models": ["819LMB", "MYQ"] diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json index 59338cf43ae..f50099f023b 100644 --- a/homeassistant/components/myq/translations/hu.json +++ b/homeassistant/components/myq/translations/hu.json @@ -21,7 +21,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a MyQ Gateway-hez" } } } diff --git a/homeassistant/components/myq/translations/zh-Hans.json b/homeassistant/components/myq/translations/zh-Hans.json index a5f4ff11f09..db06c3cf23a 100644 --- a/homeassistant/components/myq/translations/zh-Hans.json +++ b/homeassistant/components/myq/translations/zh-Hans.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "{username} \u7684\u5bc6\u7801\u5df2\u5931\u6548\u3002" + }, "user": { "data": { "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index c7755b13512..94a9cde1df2 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,7 +1,7 @@ """Support for MySensors sensors.""" from __future__ import annotations -from datetime import datetime +from typing import Any from awesomeversion import AwesomeVersion @@ -9,7 +9,9 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import ( DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -41,68 +43,153 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utc_from_timestamp from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload -SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { - "V_TEMP": [None, None, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT], - "V_HUM": [ - PERCENTAGE, - "mdi:water-percent", - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, - ], - "V_DIMMER": [PERCENTAGE, "mdi:percent", None, None], - "V_PERCENTAGE": [PERCENTAGE, "mdi:percent", None, None], - "V_PRESSURE": [None, "mdi:gauge", None, None], - "V_FORECAST": [None, "mdi:weather-partly-cloudy", None, None], - "V_RAIN": [None, "mdi:weather-rainy", None, None], - "V_RAINRATE": [None, "mdi:weather-rainy", None, None], - "V_WIND": [None, "mdi:weather-windy", None, None], - "V_GUST": [None, "mdi:weather-windy", None, None], - "V_DIRECTION": [DEGREE, "mdi:compass", None, None], - "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram", None, None], - "V_DISTANCE": [LENGTH_METERS, "mdi:ruler", None, None], - "V_IMPEDANCE": ["ohm", None, None, None], - "V_WATT": [POWER_WATT, None, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], - "V_KWH": [ - ENERGY_KILO_WATT_HOUR, - None, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - ], - "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny", None, None], - "V_FLOW": [LENGTH_METERS, "mdi:gauge", None, None], - "V_VOLUME": [VOLUME_CUBIC_METERS, None, None, None], - "V_LEVEL": { - "S_SOUND": [SOUND_PRESSURE_DB, "mdi:volume-high", None, None], - "S_VIBRATION": [FREQUENCY_HERTZ, None, None, None], - "S_LIGHT_LEVEL": [ - LIGHT_LUX, - "mdi:white-balance-sunny", - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, - ], - }, - "V_VOLTAGE": [ - ELECTRIC_POTENTIAL_VOLT, - "mdi:flash", - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - ], - "V_CURRENT": [ - ELECTRIC_CURRENT_AMPERE, - "mdi:flash-auto", - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, - ], - "V_PH": ["pH", None, None, None], - "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None, None], - "V_EC": [CONDUCTIVITY, None, None, None], - "V_VAR": ["var", None, None, None], - "V_VA": [POWER_VOLT_AMPERE, None, None, None], +SENSORS: dict[str, SensorEntityDescription] = { + "V_TEMP": SensorEntityDescription( + key="V_TEMP", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_HUM": SensorEntityDescription( + key="V_HUM", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_DIMMER": SensorEntityDescription( + key="V_DIMMER", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + ), + "V_PERCENTAGE": SensorEntityDescription( + key="V_PERCENTAGE", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + ), + "V_PRESSURE": SensorEntityDescription( + key="V_PRESSURE", + icon="mdi:gauge", + ), + "V_FORECAST": SensorEntityDescription( + key="V_FORECAST", + icon="mdi:weather-partly-cloudy", + ), + "V_RAIN": SensorEntityDescription( + key="V_RAIN", + icon="mdi:weather-rainy", + ), + "V_RAINRATE": SensorEntityDescription( + key="V_RAINRATE", + icon="mdi:weather-rainy", + ), + "V_WIND": SensorEntityDescription( + key="V_WIND", + icon="mdi:weather-windy", + ), + "V_GUST": SensorEntityDescription( + key="V_GUST", + icon="mdi:weather-windy", + ), + "V_DIRECTION": SensorEntityDescription( + key="V_DIRECTION", + native_unit_of_measurement=DEGREE, + icon="mdi:compass", + ), + "V_WEIGHT": SensorEntityDescription( + key="V_WEIGHT", + native_unit_of_measurement=MASS_KILOGRAMS, + icon="mdi:weight-kilogram", + ), + "V_DISTANCE": SensorEntityDescription( + key="V_DISTANCE", + native_unit_of_measurement=LENGTH_METERS, + icon="mdi:ruler", + ), + "V_IMPEDANCE": SensorEntityDescription( + key="V_IMPEDANCE", + native_unit_of_measurement="ohm", + ), + "V_WATT": SensorEntityDescription( + key="V_WATT", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_KWH": SensorEntityDescription( + key="V_KWH", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "V_LIGHT_LEVEL": SensorEntityDescription( + key="V_LIGHT_LEVEL", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:white-balance-sunny", + ), + "V_FLOW": SensorEntityDescription( + key="V_FLOW", + native_unit_of_measurement=LENGTH_METERS, + icon="mdi:gauge", + ), + "V_VOLUME": SensorEntityDescription( + key="V_VOLUME", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + ), + "V_LEVEL_S_SOUND": SensorEntityDescription( + key="V_LEVEL_S_SOUND", + native_unit_of_measurement=SOUND_PRESSURE_DB, + icon="mdi:volume-high", + ), + "V_LEVEL_S_VIBRATION": SensorEntityDescription( + key="V_LEVEL_S_VIBRATION", + native_unit_of_measurement=FREQUENCY_HERTZ, + ), + "V_LEVEL_S_LIGHT_LEVEL": SensorEntityDescription( + key="V_LEVEL_S_LIGHT_LEVEL", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_LEVEL_S_MOISTURE": SensorEntityDescription( + key="V_LEVEL_S_MOISTURE", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:water-percent", + ), + "V_VOLTAGE": SensorEntityDescription( + key="V_VOLTAGE", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_CURRENT": SensorEntityDescription( + key="V_CURRENT", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_PH": SensorEntityDescription( + key="V_PH", + native_unit_of_measurement="pH", + ), + "V_ORP": SensorEntityDescription( + key="V_ORP", + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + ), + "V_EC": SensorEntityDescription( + key="V_EC", + native_unit_of_measurement=CONDUCTIVITY, + ), + "V_VAR": SensorEntityDescription( + key="V_VAR", + native_unit_of_measurement="var", + ), + "V_VA": SensorEntityDescription( + key="V_VA", + native_unit_of_measurement=POWER_VOLT_AMPERE, + ), } @@ -137,46 +224,21 @@ async def async_setup_entry( class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Representation of a MySensors Sensor child node.""" - @property - def force_update(self) -> bool: - """Return True if state updates should be forced. + _attr_force_update = True - If True, a state change will be triggered anytime the state property is - updated, not just when the value changes. - """ - return True + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Set up the instance.""" + super().__init__(*args, **kwargs) + if entity_description := self._get_entity_description(): + self.entity_description = entity_description @property - def state(self) -> str | None: - """Return the state of this entity.""" + def native_value(self) -> str | None: + """Return the state of the sensor.""" return self._values.get(self.value_type) @property - def device_class(self) -> str | None: - """Return the device class of this entity.""" - return self._get_sensor_type()[2] - - @property - def icon(self) -> str | None: - """Return the icon to use in the frontend, if any.""" - return self._get_sensor_type()[1] - - @property - def last_reset(self) -> datetime | None: - """Return the time when the sensor was last reset, if any.""" - set_req = self.gateway.const.SetReq - - if set_req(self.value_type).name == "V_KWH": - return utc_from_timestamp(0) - return None - - @property - def state_class(self) -> str | None: - """Return the state class of this entity.""" - return self._get_sensor_type()[3] - - @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( @@ -191,21 +253,19 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): return TEMP_CELSIUS return TEMP_FAHRENHEIT - unit = self._get_sensor_type()[0] - return unit + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None - def _get_sensor_type(self) -> list[str | None]: - """Return list with unit and icon of sensor type.""" - pres = self.gateway.const.Presentation + def _get_entity_description(self) -> SensorEntityDescription | None: + """Return the sensor entity description.""" set_req = self.gateway.const.SetReq + entity_description = SENSORS.get(set_req(self.value_type).name) - _sensor_type = SENSORS.get( - set_req(self.value_type).name, [None, None, None, None] - ) - if isinstance(_sensor_type, dict): - sensor_type = _sensor_type.get( - pres(self.child_type).name, [None, None, None, None] + if not entity_description: + pres = self.gateway.const.Presentation + entity_description = SENSORS.get( + f"{set_req(self.value_type).name}_{pres(self.child_type).name}" ) - else: - sensor_type = _sensor_type - return sensor_type + + return entity_description diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 85472deba06..da4831de9e5 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -13,6 +13,9 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, @@ -65,133 +68,133 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key=ATTR_BME280_HUMIDITY, name=f"{DEFAULT_NAME} BME280 Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_PRESSURE, name=f"{DEFAULT_NAME} BME280 Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_TEMPERATURE, name=f"{DEFAULT_NAME} BME280 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_PRESSURE, name=f"{DEFAULT_NAME} BMP280 Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_TEMPERATURE, name=f"{DEFAULT_NAME} BMP280 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_HUMIDITY, name=f"{DEFAULT_NAME} HECA Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_TEMPERATURE, name=f"{DEFAULT_NAME} HECA Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MHZ14A_CARBON_DIOXIDE, name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P1, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P2, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_HUMIDITY, name=f"{DEFAULT_NAME} SHT3X Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_TEMPERATURE, name=f"{DEFAULT_NAME} SHT3X Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P0, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P1, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P2, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P4, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:molecule", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_HUMIDITY, name=f"{DEFAULT_NAME} DHT22 Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_TEMPERATURE, name=f"{DEFAULT_NAME} DHT22 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, name=f"{DEFAULT_NAME} Signal Strength", - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 298f88d5c29..c5c9c9f2e77 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -75,7 +75,7 @@ class NAMSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) @@ -99,7 +99,7 @@ class NAMSensorUptime(NAMSensor): """Define an Nettigo Air Monitor uptime sensor.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" uptime_sec = getattr(self.coordinator.data, self.entity_description.key) return ( diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 28569e0f1d7..6310e81cdd0 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,7 +1,7 @@ """Support for Neato botvac connected vacuum cleaners.""" -from datetime import timedelta import logging +import aiohttp from pybotvac import Account, Neato from pybotvac.exceptions import NeatoException import voluptuous as vol @@ -12,17 +12,10 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle from . import api, config_flow -from .const import ( - NEATO_CONFIG, - NEATO_DOMAIN, - NEATO_LOGIN, - NEATO_MAP_DATA, - NEATO_PERSISTENT_MAPS, - NEATO_ROBOTS, -) +from .const import NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN +from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -77,10 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as ex: + _LOGGER.debug("API error: %s (%s)", ex.code, ex.message) + if ex.code in (401, 403): + raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex + raise ConfigEntryNotReady from ex + neato_session = api.ConfigEntryAuth(hass, entry, implementation) hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session hub = NeatoHub(hass, Account(neato_session)) + await hub.async_update_entry_unique_id(entry) + try: await hass.async_add_executor_job(hub.update_robots) except NeatoException as ex: @@ -94,32 +98,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[NEATO_DOMAIN].pop(entry.entry_id) return unload_ok - - -class NeatoHub: - """A My Neato hub wrapper class.""" - - def __init__(self, hass: HomeAssistant, neato: Account) -> None: - """Initialize the Neato hub.""" - self._hass = hass - self.my_neato: Account = neato - - @Throttle(timedelta(minutes=1)) - def update_robots(self): - """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - - def download_map(self, url): - """Download a new map image.""" - map_image_data = self.my_neato.get_map_image(url) - return map_image_data diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index a22b1b48e74..cd26b009040 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -1,5 +1,8 @@ """API for Neato Botvac bound to Home Assistant OAuth.""" +from __future__ import annotations + from asyncio import run_coroutine_threadsafe +from typing import Any import pybotvac @@ -7,7 +10,7 @@ from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntryAuth(pybotvac.OAuthSession): +class ConfigEntryAuth(pybotvac.OAuthSession): # type: ignore[misc] """Provide Neato Botvac authentication tied to an OAuth2 based config entry.""" def __init__( @@ -29,7 +32,7 @@ class ConfigEntryAuth(pybotvac.OAuthSession): self.session.async_ensure_token_valid(), self.hass.loop ).result() - return self.session.token["access_token"] + return self.session.token["access_token"] # type: ignore[no-any-return] class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): @@ -39,7 +42,7 @@ class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): """ @property - def extra_authorize_data(self) -> dict: + def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return {"client_secret": self.client_secret} diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 9a2f47bcfa3..392d586068d 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -1,10 +1,20 @@ """Support for loading picture from Neato.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pybotvac.exceptions import NeatoRobotException +from pybotvac.robot import Robot +from urllib3.response import HTTPResponse from homeassistant.components.camera import Camera +from homeassistant.components.neato import NeatoHub +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( NEATO_DOMAIN, @@ -20,11 +30,13 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) ATTR_GENERATED_AT = "generated_at" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Neato camera with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) - mapdata = hass.data.get(NEATO_MAP_DATA) + neato: NeatoHub = hass.data[NEATO_LOGIN] + mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: dev.append(NeatoCleaningMap(neato, robot, mapdata)) @@ -39,7 +51,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" - def __init__(self, neato, robot, mapdata): + def __init__( + self, neato: NeatoHub, robot: Robot, mapdata: dict[str, Any] | None + ) -> None: """Initialize Neato cleaning map.""" super().__init__() self.robot = robot @@ -47,24 +61,20 @@ class NeatoCleaningMap(Camera): self._mapdata = mapdata self._available = neato is not None self._robot_name = f"{self.robot.name} Cleaning Map" - self._robot_serial = self.robot.serial - self._generated_at = None - self._image_url = None - self._image = None + self._robot_serial: str = self.robot.serial + self._generated_at: str | None = None + self._image_url: str | None = None + self._image: bytes | None = None - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" self.update() return self._image - def update(self): + def update(self) -> None: """Check the contents of the map list.""" - if self.neato is None: - _LOGGER.error("Error while updating '%s'", self.entity_id) - self._image = None - self._image_url = None - self._available = False - return _LOGGER.debug("Running camera update for '%s'", self.entity_id) try: @@ -80,7 +90,8 @@ class NeatoCleaningMap(Camera): return image_url = None - map_data = self._mapdata[self._robot_serial]["maps"][0] + if self._mapdata: + map_data: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0] image_url = map_data["url"] if image_url == self._image_url: _LOGGER.debug( @@ -89,7 +100,7 @@ class NeatoCleaningMap(Camera): return try: - image = self.neato.download_map(image_url) + image: HTTPResponse = self.neato.download_map(image_url) except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error( @@ -102,33 +113,33 @@ class NeatoCleaningMap(Camera): self._image = image.read() self._image_url = image_url - self._generated_at = map_data["generated_at"] + self._generated_at = map_data.get("generated_at") self._available = True @property - def name(self): + def name(self) -> str: """Return the name of this camera.""" return self._robot_name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID.""" return self._robot_serial @property - def available(self): + def available(self) -> bool: """Return if the robot is available.""" return self._available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" - data = {} + data: dict[str, Any] = {} if self._generated_at is not None: data[ATTR_GENERATED_AT] = self._generated_at diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 580faffe8ff..07aea0a7e9c 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -2,10 +2,13 @@ from __future__ import annotations import logging +from types import MappingProxyType +from typing import Any import voluptuous as vol -from homeassistant.const import CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import NEATO_DOMAIN @@ -23,20 +26,24 @@ class OAuth2FlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_user(self, user_input: dict | None = None) -> dict: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create an entry for the flow.""" current_entries = self._async_current_entries() - if current_entries and CONF_TOKEN in current_entries[0].data: + if self.source != SOURCE_REAUTH and current_entries: # Already configured return self.async_abort(reason="already_configured") return await super().async_step_user(user_input=user_input) - async def async_step_reauth(self, data) -> dict: + async def async_step_reauth(self, data: MappingProxyType[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input: dict | None = None) -> dict: + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm reauth upon migration of old entries.""" if user_input is None: return self.async_show_form( @@ -44,10 +51,10 @@ class OAuth2FlowHandler( ) return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow. Update an entry if one already exist.""" current_entries = self._async_current_entries() - if current_entries and CONF_TOKEN not in current_entries[0].data: + if self.source == SOURCE_REAUTH and current_entries: # Update entry self.hass.config_entries.async_update_entry( current_entries[0], title=self.flow_impl.name, data=data diff --git a/homeassistant/components/neato/hub.py b/homeassistant/components/neato/hub.py new file mode 100644 index 00000000000..cb639de4acb --- /dev/null +++ b/homeassistant/components/neato/hub.py @@ -0,0 +1,49 @@ +"""Support for Neato botvac connected vacuum cleaners.""" +from datetime import timedelta +import logging + +from pybotvac import Account +from urllib3.response import HTTPResponse + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import Throttle + +from .const import NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS + +_LOGGER = logging.getLogger(__name__) + + +class NeatoHub: + """A My Neato hub wrapper class.""" + + def __init__(self, hass: HomeAssistant, neato: Account) -> None: + """Initialize the Neato hub.""" + self._hass = hass + self.my_neato: Account = neato + + @Throttle(timedelta(minutes=1)) + def update_robots(self) -> None: + """Update the robot states.""" + _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + def download_map(self, url: str) -> HTTPResponse: + """Download a new map image.""" + map_image_data = self.my_neato.get_map_image(url) + return map_image_data + + async def async_update_entry_unique_id(self, entry: ConfigEntry) -> str: + """Update entry for unique_id.""" + + await self._hass.async_add_executor_job(self.my_neato.refresh_userdata) + unique_id: str = self.my_neato.unique_id + + if entry.unique_id == unique_id: + return unique_id + + _LOGGER.debug("Updating user unique_id for previous config entry") + self._hass.config_entries.async_update_entry(entry, unique_id=unique_id) + return unique_id diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 014e366db46..fc751df45de 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,7 +3,7 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": ["pybotvac==0.0.21"], + "requirements": ["pybotvac==0.0.22"], "codeowners": ["@dshokouhi", "@Santobert"], "dependencies": ["http"], "iot_class": "cloud_polling" diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 98208698037..2d54e89bb04 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -1,11 +1,20 @@ """Support for Neato sensors.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pybotvac.exceptions import NeatoRobotException +from pybotvac.robot import Robot +from homeassistant.components.neato import NeatoHub from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -16,10 +25,12 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) BATTERY = "Battery" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Neato sensor using config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) + neato: NeatoHub = hass.data[NEATO_LOGIN] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoSensor(neato, robot)) @@ -33,15 +44,15 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoSensor(SensorEntity): """Neato sensor.""" - def __init__(self, neato, robot): + def __init__(self, neato: NeatoHub, robot: Robot) -> None: """Initialize Neato sensor.""" self.robot = robot - self._available = False - self._robot_name = f"{self.robot.name} {BATTERY}" - self._robot_serial = self.robot.serial - self._state = None + self._available: bool = False + self._robot_name: str = f"{self.robot.name} {BATTERY}" + self._robot_serial: str = self.robot.serial + self._state: dict[str, Any] | None = None - def update(self): + def update(self) -> None: """Update Neato Sensor.""" try: self._state = self.robot.state @@ -58,36 +69,38 @@ class NeatoSensor(SensorEntity): _LOGGER.debug("self._state=%s", self._state) @property - def name(self): + def name(self) -> str: """Return the name of this sensor.""" return self._robot_name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID.""" return self._robot_serial @property - def device_class(self): + def device_class(self) -> str: """Return the device class.""" return DEVICE_CLASS_BATTERY @property - def available(self): + def available(self) -> bool: """Return availability.""" return self._available @property - def state(self): + def native_value(self) -> str | None: """Return the state.""" - return self._state["details"]["charge"] if self._state else None + if self._state is not None: + return str(self._state["details"]["charge"]) + return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index a3cc51b82c6..0e0d49f2b28 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -1,11 +1,19 @@ """Support for Neato Connected Vacuums switches.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pybotvac.exceptions import NeatoRobotException +from pybotvac.robot import Robot +from homeassistant.components.neato import NeatoHub +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, ToggleEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -18,10 +26,13 @@ SWITCH_TYPE_SCHEDULE = "schedule" SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Neato switch with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) + neato: NeatoHub = hass.data[NEATO_LOGIN] + for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: dev.append(NeatoConnectedSwitch(neato, robot, type_name)) @@ -36,18 +47,18 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedSwitch(ToggleEntity): """Neato Connected Switches.""" - def __init__(self, neato, robot, switch_type): + def __init__(self, neato: NeatoHub, robot: Robot, switch_type: str) -> None: """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot self._available = False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" - self._state = None - self._schedule_state = None + self._state: dict[str, Any] | None = None + self._schedule_state: str | None = None self._clean_state = None - self._robot_serial = self.robot.serial + self._robot_serial: str = self.robot.serial - def update(self): + def update(self) -> None: """Update the states of Neato switches.""" _LOGGER.debug("Running Neato switch update for '%s'", self.entity_id) try: @@ -65,7 +76,7 @@ class NeatoConnectedSwitch(ToggleEntity): _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) - if self._state["details"]["isScheduleEnabled"]: + if self._state is not None and self._state["details"]["isScheduleEnabled"]: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF @@ -74,34 +85,33 @@ class NeatoConnectedSwitch(ToggleEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return self._robot_name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._available @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._robot_serial @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - if self.type == SWITCH_TYPE_SCHEDULE: - if self._schedule_state == STATE_ON: - return True - return False + return bool( + self.type == SWITCH_TYPE_SCHEDULE and self._schedule_state == STATE_ON + ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: try: @@ -111,7 +121,7 @@ class NeatoConnectedSwitch(ToggleEntity): "Neato switch connection error '%s': %s", self.entity_id, ex ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self.type == SWITCH_TYPE_SCHEDULE: try: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index b6cf43a6a3e..527cd4dce23 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -1,7 +1,11 @@ """Support for Neato Connected Vacuums.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any +from pybotvac import Robot from pybotvac.exceptions import NeatoRobotException import voluptuous as vol @@ -24,9 +28,14 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NeatoHub from .const import ( ACTION, ALERTS, @@ -72,12 +81,14 @@ ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Neato vacuum with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) - mapdata = hass.data.get(NEATO_MAP_DATA) - persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS) + neato: NeatoHub = hass.data[NEATO_LOGIN] + mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) + persistent_maps: dict[str, Any] | None = hass.data.get(NEATO_PERSISTENT_MAPS) for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) @@ -105,33 +116,39 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedVacuum(StateVacuumEntity): """Representation of a Neato Connected Vacuum.""" - def __init__(self, neato, robot, mapdata, persistent_maps): + def __init__( + self, + neato: NeatoHub, + robot: Robot, + mapdata: dict[str, Any] | None, + persistent_maps: dict[str, Any] | None, + ) -> None: """Initialize the Neato Connected Vacuum.""" self.robot = robot - self._available = neato is not None + self._available: bool = neato is not None self._mapdata = mapdata - self._name = f"{self.robot.name}" - self._robot_has_map = self.robot.has_persistent_maps + self._name: str = f"{self.robot.name}" + self._robot_has_map: bool = self.robot.has_persistent_maps self._robot_maps = persistent_maps - self._robot_serial = self.robot.serial - self._status_state = None - self._clean_state = None - self._state = None - self._clean_time_start = None - self._clean_time_stop = None - self._clean_area = None - self._clean_battery_start = None - self._clean_battery_end = None - self._clean_susp_charge_count = None - self._clean_susp_time = None - self._clean_pause_time = None - self._clean_error_time = None - self._launched_from = None - self._battery_level = None - self._robot_boundaries = [] - self._robot_stats = None + self._robot_serial: str = self.robot.serial + self._status_state: str | None = None + self._clean_state: str | None = None + self._state: dict[str, Any] | None = None + self._clean_time_start: str | None = None + self._clean_time_stop: str | None = None + self._clean_area: float | None = None + self._clean_battery_start: int | None = None + self._clean_battery_end: int | None = None + self._clean_susp_charge_count: int | None = None + self._clean_susp_time: int | None = None + self._clean_pause_time: int | None = None + self._clean_error_time: int | None = None + self._launched_from: str | None = None + self._battery_level: int | None = None + self._robot_boundaries: list = [] + self._robot_stats: dict[str, Any] | None = None - def update(self): + def update(self) -> None: """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update for '%s'", self.entity_id) try: @@ -151,6 +168,8 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._available = False return + if self._state is None: + return self._available = True _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: @@ -198,10 +217,12 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._battery_level = self._state["details"]["charge"] - if not self._mapdata.get(self._robot_serial, {}).get("maps", []): + if self._mapdata is None or not self._mapdata.get(self._robot_serial, {}).get( + "maps", [] + ): return - mapdata = self._mapdata[self._robot_serial]["maps"][0] + mapdata: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0] self._clean_time_start = mapdata["start_at"] self._clean_time_stop = mapdata["end_at"] self._clean_area = mapdata["cleaned_area"] @@ -215,10 +236,11 @@ class NeatoConnectedVacuum(StateVacuumEntity): if ( self._robot_has_map + and self._state and self._state["availableServices"]["maps"] != "basic-1" - and self._robot_maps[self._robot_serial] + and self._robot_maps ): - allmaps = self._robot_maps[self._robot_serial] + allmaps: dict = self._robot_maps[self._robot_serial] _LOGGER.debug( "Found the following maps for '%s': %s", self.entity_id, allmaps ) @@ -249,44 +271,44 @@ class NeatoConnectedVacuum(StateVacuumEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def supported_features(self): + def supported_features(self) -> int: """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_NEATO @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" return self._battery_level @property - def available(self): + def available(self) -> bool: """Return if the robot is available.""" return self._available @property - def icon(self): + def icon(self) -> str: """Return neato specific icon.""" return "mdi:robot-vacuum-variant" @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" return self._clean_state @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._robot_serial @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" - data = {} + data: dict[str, Any] = {} if self._status_state is not None: data[ATTR_STATUS] = self._status_state @@ -314,28 +336,32 @@ class NeatoConnectedVacuum(StateVacuumEntity): return data @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} + info: DeviceInfo = { + "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, + "name": self._name, + } if self._robot_stats: info["manufacturer"] = self._robot_stats["battery"]["vendor"] info["model"] = self._robot_stats["model"] info["sw_version"] = self._robot_stats["firmware"] return info - def start(self): + def start(self) -> None: """Start cleaning or resume cleaning.""" - try: - if self._state["state"] == 1: - self.robot.start_cleaning() - elif self._state["state"] == 3: - self.robot.resume_cleaning() - except NeatoRobotException as ex: - _LOGGER.error( - "Neato vacuum connection error for '%s': %s", self.entity_id, ex - ) + if self._state: + try: + if self._state["state"] == 1: + self.robot.start_cleaning() + elif self._state["state"] == 3: + self.robot.resume_cleaning() + except NeatoRobotException as ex: + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) - def pause(self): + def pause(self) -> None: """Pause the vacuum.""" try: self.robot.pause_cleaning() @@ -344,7 +370,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" try: if self._clean_state == STATE_CLEANING: @@ -356,7 +382,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" try: self.robot.stop_cleaning() @@ -365,7 +391,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the robot by making it emit a sound.""" try: self.robot.locate() @@ -374,7 +400,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Run a spot cleaning starting from the base.""" try: self.robot.start_spot_cleaning() @@ -383,7 +409,9 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def neato_custom_cleaning(self, mode, navigation, category, zone=None): + def neato_custom_cleaning( + self, mode: str, navigation: str, category: str, zone: str | None = None + ) -> None: """Zone cleaning service call.""" boundary_id = None if zone is not None: diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index de8a85f44fd..8cbe7b1f803 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -118,7 +118,7 @@ class NSDepartureSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b999b2e94e0..ff340d38424 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -69,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["sensor", "camera", "climate"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 5f5fdbc8d93..242c6147201 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -180,7 +180,9 @@ class NestCamera(Camera): self._device.add_update_listener(self.async_write_ha_state) ) - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" # Returns the snapshot of the last event for ~30 seconds after the event active_event_image = await self._async_active_event_image() diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 6278547f216..383c6d22258 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -40,7 +40,7 @@ class NestDeviceInfo: ) @property - def device_name(self) -> str: + def device_name(self) -> str | None: """Return the name of the physical device that includes the sensor.""" if InfoTrait.NAME in self._device.traits: trait: InfoTrait = self._device.traits[InfoTrait.NAME] @@ -56,11 +56,9 @@ class NestDeviceInfo: return self.device_model @property - def device_model(self) -> str: + def device_model(self) -> str | None: """Return device model information.""" # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - if self._device.type in DEVICE_TYPE_MAP: - return DEVICE_TYPE_MAP[self._device.type] - return "Unknown" + return DEVICE_TYPE_MAP.get(self._device.type) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 04f7b1ac663..76ecf16b67b 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -9,6 +9,7 @@ from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -17,7 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity @@ -96,7 +97,7 @@ def nest_update_event_broker(hass, nest): _LOGGER.debug("Stop listening for nest.update_event") -async def async_setup_legacy(hass, config) -> bool: +async def async_setup_legacy(hass: HomeAssistant, config: dict) -> bool: """Set up Nest components using the legacy nest API.""" if DOMAIN not in config: return True @@ -122,7 +123,7 @@ async def async_setup_legacy(hass, config) -> bool: return True -async def async_setup_legacy_entry(hass, entry) -> bool: +async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from legacy config entry.""" nest = Nest(access_token=entry.data["tokens"]["access_token"]) diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 77629e4dcff..3ef0089d2bc 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -1,4 +1,6 @@ """Support for Nest Cameras.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -131,7 +133,9 @@ class NestCamera(Camera): def _ready_for_snapshot(self, now): return self._next_snapshot_at is None or now > self._next_snapshot_at - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" now = utcnow() if self._ready_for_snapshot(now): diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 0939e925b43..f2c6670bf8b 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -154,12 +154,12 @@ class NestBasicSensor(NestSensorDevice, SensorEntity): """Representation a basic Nest sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -189,12 +189,12 @@ class NestTempSensor(NestSensorDevice, SensorEntity): """Representation of a Nest Temperature sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 6c9462e43db..5b078393d1e 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.5"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.6"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 42614af8c40..0034acff3af 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -95,13 +95,13 @@ class TemperatureSensor(SensorBase): return f"{self._device_info.device_name} Temperature" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @@ -126,13 +126,13 @@ class HumiditySensor(SensorBase): return f"{self._device_info.device_name} Humidity" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] return trait.ambient_humidity_percent @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/nest/translations/da.json b/homeassistant/components/nest/translations/da.json index 054b4442506..5224e7a660d 100644 --- a/homeassistant/components/nest/translations/da.json +++ b/homeassistant/components/nest/translations/da.json @@ -14,7 +14,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", - "title": "Godkendelsesudbyder" + "title": "Identitetsudbyder" }, "link": { "data": { diff --git a/homeassistant/components/nest/translations/lt.json b/homeassistant/components/nest/translations/lt.json index 3cac49e3871..629b65d347d 100644 --- a/homeassistant/components/nest/translations/lt.json +++ b/homeassistant/components/nest/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Nenumatyta klaida" + }, "step": { "link": { "data": { diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index edb8837fd18..76a5eeb9c86 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import ( @@ -69,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Netatmo component.""" hass.data[DOMAIN] = { DATA_PERSONS: {}, diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 32d0eb46286..4d6141e2dfb 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -194,10 +194,14 @@ class NetatmoCamera(NetatmoBase, Camera): self.data_handler.data[self._data_classes[0]["name"]], ) - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: - return await self._data.async_get_live_snapshot(camera_id=self._id) + return cast( + bytes, await self._data.async_get_live_snapshot(camera_id=self._id) + ) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 0c55b459847..a1f7b2ac079 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -83,7 +83,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Temperature", netatmo_name="Temperature", entity_registry_enabled_default=True, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( key="co2", name="CO2", netatmo_name="CO2", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, @@ -108,7 +108,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Pressure", netatmo_name="Pressure", entity_registry_enabled_default=True, - unit_of_measurement=PRESSURE_MBAR, + native_unit_of_measurement=PRESSURE_MBAR, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -124,7 +124,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Noise", netatmo_name="Noise", entity_registry_enabled_default=True, - unit_of_measurement=SOUND_PRESSURE_DB, + native_unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", state_class=STATE_CLASS_MEASUREMENT, ), @@ -133,7 +133,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Humidity", netatmo_name="Humidity", entity_registry_enabled_default=True, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -142,7 +142,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain", netatmo_name="Rain", entity_registry_enabled_default=True, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -150,7 +150,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain last hour", netatmo_name="sum_rain_1", entity_registry_enabled_default=False, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -158,7 +158,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain today", netatmo_name="sum_rain_24", entity_registry_enabled_default=True, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -166,7 +166,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Battery Percent", netatmo_name="battery_percent", entity_registry_enabled_default=True, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -182,7 +182,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Angle", netatmo_name="WindAngle", entity_registry_enabled_default=False, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", state_class=STATE_CLASS_MEASUREMENT, ), @@ -191,7 +191,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wind Strength", netatmo_name="WindStrength", entity_registry_enabled_default=True, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT, ), @@ -207,7 +207,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Gust Angle", netatmo_name="GustAngle", entity_registry_enabled_default=False, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", state_class=STATE_CLASS_MEASUREMENT, ), @@ -216,7 +216,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Gust Strength", netatmo_name="GustStrength", entity_registry_enabled_default=False, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT, ), @@ -239,7 +239,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio Level", netatmo_name="rf_status", entity_registry_enabled_default=False, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), @@ -255,7 +255,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi Level", netatmo_name="wifi_status", entity_registry_enabled_default=False, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), @@ -518,25 +518,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._device_name, self._id, ) - self._attr_state = None + self._attr_native_value = None return try: state = data[self.entity_description.netatmo_name] if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}: - self._attr_state = round(state, 1) + self._attr_native_value = round(state, 1) elif self.entity_description.key in {"windangle_value", "gustangle_value"}: - self._attr_state = fix_angle(state) + self._attr_native_value = fix_angle(state) elif self.entity_description.key in {"windangle", "gustangle"}: - self._attr_state = process_angle(fix_angle(state)) + self._attr_native_value = process_angle(fix_angle(state)) elif self.entity_description.key == "rf_status": - self._attr_state = process_rf(state) + self._attr_native_value = process_rf(state) elif self.entity_description.key == "wifi_status": - self._attr_state = process_wifi(state) + self._attr_native_value = process_wifi(state) elif self.entity_description.key == "health_idx": - self._attr_state = process_health(state) + self._attr_native_value = process_health(state) else: - self._attr_state = state + self._attr_native_value = state except KeyError: if self.state: _LOGGER.debug( @@ -544,7 +544,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._device_name, ) - self._attr_state = None + self._attr_native_value = None return self.async_write_ha_state() @@ -758,14 +758,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._area_name, ) - self._attr_state = None + self._attr_native_value = None return if values := [x for x in data.values() if x is not None]: if self._mode == "avg": - self._attr_state = round(sum(values) / len(values), 1) + self._attr_native_value = round(sum(values) / len(values), 1) elif self._mode == "max": - self._attr_state = max(values) + self._attr_native_value = max(values) self._attr_available = self.state is not None self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index 0e6536bb0ad..48f084f84c2 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -41,14 +41,24 @@ "step": { "public_weather": { "data": { - "area_name": "A ter\u00fclet neve" - } + "area_name": "A ter\u00fclet neve", + "lat_ne": "Sz\u00e9less\u00e9g \u00c9szakkeleti sarok", + "lat_sw": "Sz\u00e9less\u00e9g D\u00e9lnyugati sarok", + "lon_ne": "Hossz\u00fas\u00e1g \u00c9szakkeleti sarok", + "lon_sw": "Hossz\u00fas\u00e1g D\u00e9lnyugati sarok", + "mode": "Sz\u00e1m\u00edt\u00e1s", + "show_on_map": "Mutasd a t\u00e9rk\u00e9pen" + }, + "description": "\u00c1ll\u00edtson be egy nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151t egy ter\u00fclethez.", + "title": "Netatmo nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151" }, "public_weather_areas": { "data": { "new_area": "Ter\u00fclet neve", "weather_areas": "Id\u0151j\u00e1r\u00e1si ter\u00fcletek" - } + }, + "description": "\u00c1ll\u00edtsa be a nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151ket.", + "title": "Netatmo nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151" } } } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 21e4cd1b005..d1fa87a6e5d 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -117,7 +117,7 @@ class NetdataSensor(SensorEntity): return f"{self._name} {self._sensor_name}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -127,7 +127,7 @@ class NetdataSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state @@ -162,7 +162,7 @@ class NetdataAlarms(SensorEntity): return f"{self._name} Alarms" @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index c8f07301e98..0996ad3d315 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -37,7 +37,7 @@ class LTESensor(LTEEntity, SensorEntity): """Base LTE sensor entity.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_UNITS[self.sensor_type] @@ -46,7 +46,7 @@ class SMSUnreadSensor(LTESensor): """Unread SMS sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return sum(1 for x in self.modem_data.data.sms if x.unread) @@ -55,7 +55,7 @@ class SMSTotalSensor(LTESensor): """Total SMS sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return len(self.modem_data.data.sms) @@ -64,7 +64,7 @@ class UsageSensor(LTESensor): """Data usage sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self.modem_data.data.usage / 1024 ** 2, 1) @@ -73,6 +73,6 @@ class GenericSensor(LTESensor): """Sensor entity with raw state.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return getattr(self.modem_data.data, self.sensor_type) diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index c39b1598c89..88da77cbf90 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,7 +1,10 @@ """The Netio switch component.""" +from __future__ import annotations + from collections import namedtuple from datetime import timedelta import logging +from typing import Any from pynetio import Netio import voluptuous as vol @@ -29,8 +32,8 @@ CONF_OUTLETS = "outlets" DEFAULT_PORT = 1234 DEFAULT_USERNAME = "admin" -Device = namedtuple("device", ["netio", "entities"]) -DEVICES = {} +Device = namedtuple("Device", ["netio", "entities"]) +DEVICES: dict[str, Any] = {} MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 48903d145e7..a7dffad7084 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -1,13 +1,14 @@ """The Network Configuration integration.""" from __future__ import annotations +from ipaddress import IPv4Address, IPv6Address import logging import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -45,6 +46,35 @@ async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str: return source_ip if source_ip in all_ipv4s else all_ipv4s[0] +@bind_hass +async def async_get_enabled_source_ips( + hass: HomeAssistant, +) -> list[IPv4Address | IPv6Address]: + """Build the list of enabled source ips.""" + adapters = await async_get_adapters(hass) + sources: list[IPv4Address | IPv6Address] = [] + for adapter in adapters: + if not adapter["enabled"]: + continue + if adapter["ipv4"]: + sources.extend(IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]) + if adapter["ipv6"]: + # With python 3.9 add scope_ids can be + # added by enumerating adapter["ipv6"]s + # IPv6Address(f"::%{ipv6['scope_id']}") + sources.extend(IPv6Address(ipv6["address"]) for ipv6 in adapter["ipv6"]) + + return sources + + +@callback +def async_only_default_interface_enabled(adapters: list[Adapter]) -> bool: + """Check to see if any non-default adapter is enabled.""" + return not any( + adapter["enabled"] and not adapter["default"] for adapter in adapters + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" 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/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index d74d6338c8b..37113dde8b7 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -149,12 +149,12 @@ class NeurioEnergy(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index a14931e41ee..6e44b8c9883 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -182,7 +182,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): return self._class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" val = getattr(self._thermostat, self._call)() if self._modifier: @@ -192,7 +192,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): return val @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement @@ -230,7 +230,7 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): return self._class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" val = getattr(self._zone, self._call)() if self._modifier: @@ -240,6 +240,6 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): return val @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index fb03bcd25b5..f9df0d60412 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -146,7 +146,7 @@ class NextBusDepartureSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return current state of the sensor.""" return self._state diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 5cd02f124e9..6a2d106bb10 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -34,7 +34,7 @@ class NextcloudSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state for this sensor.""" return self._state diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 90a76c1c747..92bb492bf7d 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 @@ -9,15 +7,14 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - PLATFORMS = [NOTIFY] -async def async_setup(hass: HomeAssistant, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NFAndroidTV component.""" hass.data.setdefault(DOMAIN, {}) # Iterate all entries for notify to only get nfandroidtv @@ -41,8 +38,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/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json new file mode 100644 index 00000000000..e99ce545b74 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Esta integraci\u00f3n requiere la aplicaci\u00f3n de Notificaciones para Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDebe configurar una reserva DHCP en su router (consulte el manual de usuario de su router) o una direcci\u00f3n IP est\u00e1tica en el dispositivo. Si no, el dispositivo acabar\u00e1 por no estar disponible.", + "title": "Notificaciones para Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json new file mode 100644 index 00000000000..e7dea95e4d0 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "H\u00e1zigazda", + "name": "N\u00e9v" + }, + "description": "Ehhez az integr\u00e1ci\u00f3hoz az \u00c9rtes\u00edt\u00e9sek az Android TV alkalmaz\u00e1shoz sz\u00fcks\u00e9ges. \n\nAndroid TV eset\u00e9n: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nA Fire TV eset\u00e9ben: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nBe kell \u00e1ll\u00edtania a DHCP -foglal\u00e1st az \u00fatv\u00e1laszt\u00f3n (l\u00e1sd az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t), vagy egy statikus IP -c\u00edmet az eszk\u00f6z\u00f6n. Ha nem, az eszk\u00f6z v\u00e9g\u00fcl el\u00e9rhetetlenn\u00e9 v\u00e1lik.", + "title": "\u00c9rtes\u00edt\u00e9sek Android TV / Fire TV eset\u00e9n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/no.json b/homeassistant/components/nfandroidtv/translations/no.json new file mode 100644 index 00000000000..e8aea574c96 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn" + }, + "description": "Denne integrasjonen krever Notifications for Android TV -appen. \n\n For Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n For Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Du b\u00f8r konfigurere enten DHCP -reservasjon p\u00e5 ruteren din (se brukerh\u00e5ndboken til ruteren din) eller en statisk IP -adresse p\u00e5 enheten. Hvis ikke, vil enheten til slutt bli utilgjengelig.", + "title": "Varsler for Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 183755298d6..1b37fa8da7c 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -58,7 +58,7 @@ class NightscoutSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -68,7 +68,7 @@ class NightscoutSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state 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/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 936d607a84e..4074cd47f50 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -50,12 +50,12 @@ class LeafBatterySensor(LeafEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Battery state percentage.""" return round(self.car.data[DATA_BATTERY]) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Battery state measured in percentage.""" return PERCENTAGE @@ -89,7 +89,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Battery range in miles or kms.""" if self._ac_on: ret = self.car.data[DATA_RANGE_AC] @@ -102,7 +102,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): return round(ret) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Battery range unit.""" if not self.car.hass.config.units.is_metric or self.car.force_miles: return LENGTH_MILES diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index da699caaa73..87e9ad895af 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1 +1,393 @@ -"""The nmap_tracker component.""" +"""The Nmap Tracker integration.""" +from __future__ import annotations + +import asyncio +import contextlib +from dataclasses import dataclass +from datetime import datetime, timedelta +from functools import partial +import logging + +import aiohttp +from getmac import get_mac_address +from mac_vendor_lookup import AsyncMacLookup +from nmap import PortScanner, PortScannerError + +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DOMAIN, + NMAP_TRACKED_DEVICES, + PLATFORMS, + TRACKER_SCAN_INTERVAL, +) + +# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n' +NMAP_TRANSIENT_FAILURE = "Assertion failed: htn.toclock_running == true" +MAX_SCAN_ATTEMPTS = 16 +OFFLINE_SCANS_TO_MARK_UNAVAILABLE = 3 + + +def short_hostname(hostname): + """Return the first part of the hostname.""" + if hostname is None: + return None + return hostname.split(".")[0] + + +def human_readable_name(hostname, vendor, mac_address): + """Generate a human readable name.""" + if hostname: + return short_hostname(hostname) + if vendor: + return f"{vendor} {mac_address[-8:]}" + return f"Nmap Tracker {mac_address}" + + +@dataclass +class NmapDevice: + """Class for keeping track of an nmap tracked device.""" + + mac_address: str + hostname: str + name: str + ipv4: str + manufacturer: str + reason: str + last_update: datetime.datetime + offline_scans: int + + +class NmapTrackedDevices: + """Storage class for all nmap trackers.""" + + def __init__(self) -> None: + """Initialize the data.""" + self.tracked: dict = {} + self.ipv4_last_mac: dict = {} + self.config_entry_owner: dict = {} + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nmap Tracker from a config entry.""" + domain_data = hass.data.setdefault(DOMAIN, {}) + devices = domain_data.setdefault(NMAP_TRACKED_DEVICES, NmapTrackedDevices()) + scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) + await scanner.async_setup() + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + _async_untrack_devices(hass, entry) + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@callback +def _async_untrack_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove tracking for devices owned by this config entry.""" + devices = hass.data[DOMAIN][NMAP_TRACKED_DEVICES] + remove_mac_addresses = [ + mac_address + for mac_address, entry_id in devices.config_entry_owner.items() + if entry_id == entry.entry_id + ] + for mac_address in remove_mac_addresses: + if device := devices.tracked.pop(mac_address, None): + devices.ipv4_last_mac.pop(device.ipv4, None) + del devices.config_entry_owner[mac_address] + + +def signal_device_update(mac_address) -> str: + """Signal specific per nmap tracker entry to signal updates in device.""" + return f"{DOMAIN}-device-update-{mac_address}" + + +class NmapDeviceScanner: + """This class scans for devices using nmap.""" + + def __init__(self, hass, entry, devices): + """Initialize the scanner.""" + self.devices = devices + self.home_interval = None + + self._hass = hass + self._entry = entry + + self._scan_lock = None + self._stopping = False + self._scanner = None + + self._entry_id = entry.entry_id + self._hosts = None + self._options = None + self._exclude = None + self._scan_interval = None + + self._known_mac_addresses = {} + self._finished_first_scan = False + self._last_results = [] + self._mac_vendor_lookup = None + + async def async_setup(self): + """Set up the tracker.""" + config = self._entry.options + self._scan_interval = timedelta( + seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) + ) + hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) + self._hosts = [host for host in hosts_list if host != ""] + excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) + self._exclude = [exclude for exclude in excludes_list if exclude != ""] + self._options = config[CONF_OPTIONS] + self.home_interval = timedelta( + minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) + ) + self._scan_lock = asyncio.Lock() + if self._hass.state == CoreState.running: + await self._async_start_scanner() + return + + self._entry.async_on_unload( + self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner + ) + ) + registry = er.async_get(self._hass) + self._known_mac_addresses = { + entry.unique_id: entry.original_name + for entry in registry.entities.values() + if entry.config_entry_id == self._entry_id + } + + @property + def signal_device_new(self) -> str: + """Signal specific per nmap tracker entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._entry_id}" + + @property + def signal_device_missing(self) -> str: + """Signal specific per nmap tracker entry to signal a missing device.""" + return f"{DOMAIN}-device-missing-{self._entry_id}" + + @callback + def _async_get_vendor(self, mac_address): + """Lookup the vendor.""" + oui = self._mac_vendor_lookup.sanitise(mac_address)[:6] + return self._mac_vendor_lookup.prefixes.get(oui) + + @callback + def _async_stop(self): + """Stop the scanner.""" + self._stopping = True + + async def _async_start_scanner(self, *_): + """Start the scanner.""" + self._entry.async_on_unload(self._async_stop) + self._entry.async_on_unload( + async_track_time_interval( + self._hass, + self._async_scan_devices, + self._scan_interval, + ) + ) + self._mac_vendor_lookup = AsyncMacLookup() + with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): + # We don't care if this fails since it only + # improves the data when we don't have it from nmap + await self._mac_vendor_lookup.load_vendors() + self._hass.async_create_task(self._async_scan_devices()) + + def _build_options(self): + """Build the command line and strip out last results that do not need to be updated.""" + options = self._options + if self.home_interval: + boundary = dt_util.now() - self.home_interval + last_results = [ + device for device in self._last_results if device.last_update > boundary + ] + if last_results: + exclude_hosts = self._exclude + [device.ipv4 for device in last_results] + else: + exclude_hosts = self._exclude + else: + last_results = [] + exclude_hosts = self._exclude + if exclude_hosts: + options += f" --exclude {','.join(exclude_hosts)}" + # Report reason + if "--reason" not in options: + options += " --reason" + # Report down hosts + if "-v" not in options: + options += " -v" + self._last_results = last_results + return options + + async def _async_scan_devices(self, *_): + """Scan devices and dispatch.""" + if self._scan_lock.locked(): + _LOGGER.debug( + "Nmap scanning is taking longer than the scheduled interval: %s", + TRACKER_SCAN_INTERVAL, + ) + return + + async with self._scan_lock: + try: + await self._async_run_nmap_scan() + except PortScannerError as ex: + _LOGGER.error("Nmap scanning failed: %s", ex) + + if not self._finished_first_scan: + self._finished_first_scan = True + await self._async_mark_missing_devices_as_not_home() + + async def _async_mark_missing_devices_as_not_home(self): + # After all config entries have finished their first + # scan we mark devices that were not found as not_home + # from unavailable + now = dt_util.now() + for mac_address, original_name in self._known_mac_addresses.items(): + if mac_address in self.devices.tracked: + continue + self.devices.config_entry_owner[mac_address] = self._entry_id + self.devices.tracked[mac_address] = NmapDevice( + mac_address, + None, + original_name, + None, + self._async_get_vendor(mac_address), + "Device not found in initial scan", + now, + 1, + ) + async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) + + def _run_nmap_scan(self): + """Run nmap and return the result.""" + options = self._build_options() + if not self._scanner: + self._scanner = PortScanner() + _LOGGER.debug("Scanning %s with args: %s", self._hosts, options) + for attempt in range(MAX_SCAN_ATTEMPTS): + try: + result = self._scanner.scan( + hosts=" ".join(self._hosts), + arguments=options, + timeout=TRACKER_SCAN_INTERVAL * 10, + ) + break + except PortScannerError as ex: + if attempt < (MAX_SCAN_ATTEMPTS - 1) and NMAP_TRANSIENT_FAILURE in str( + ex + ): + _LOGGER.debug("Nmap saw transient error %s", NMAP_TRANSIENT_FAILURE) + continue + raise + _LOGGER.debug( + "Finished scanning %s with args: %s", + self._hosts, + options, + ) + return result + + @callback + def _async_increment_device_offline(self, ipv4, reason): + """Mark an IP offline.""" + if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)): + return + if not (device := self.devices.tracked.get(formatted_mac)): + # Device was unloaded + return + device.offline_scans += 1 + if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE: + return + device.reason = reason + async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False) + del self.devices.ipv4_last_mac[ipv4] + + async def _async_run_nmap_scan(self): + """Scan the network for devices and dispatch events.""" + result = await self._hass.async_add_executor_job(self._run_nmap_scan) + if self._stopping: + return + + devices = self.devices + entry_id = self._entry_id + now = dt_util.now() + for ipv4, info in result["scan"].items(): + status = info["status"] + reason = status["reason"] + if status["state"] != "up": + self._async_increment_device_offline(ipv4, reason) + continue + # Mac address only returned if nmap ran as root + mac = info["addresses"].get( + "mac" + ) or await self._hass.async_add_executor_job( + partial(get_mac_address, ip=ipv4) + ) + if mac is None: + self._async_increment_device_offline(ipv4, "No MAC address found") + _LOGGER.info("No MAC address found for %s", ipv4) + continue + + formatted_mac = format_mac(mac) + new = formatted_mac not in devices.tracked + if ( + new + and formatted_mac not in devices.tracked + and formatted_mac not in self._known_mac_addresses + ): + continue + + if ( + devices.config_entry_owner.setdefault(formatted_mac, entry_id) + != entry_id + ): + continue + + hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 + vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) + name = human_readable_name(hostname, vendor, mac) + device = NmapDevice( + formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 + ) + + devices.tracked[formatted_mac] = device + devices.ipv4_last_mac[ipv4] = formatted_mac + self._last_results.append(device) + + if new: + async_dispatcher_send(self._hass, self.signal_device_new, formatted_mac) + else: + async_dispatcher_send( + self._hass, signal_device_update(formatted_mac), True + ) diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py new file mode 100644 index 00000000000..eaea87e775a --- /dev/null +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -0,0 +1,215 @@ +"""Config flow for Nmap Tracker integration.""" +from __future__ import annotations + +from ipaddress import ip_address, ip_network, summarize_address_range +from typing import Any + +import ifaddr +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.util import get_local_ip + +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) + +DEFAULT_NETWORK_PREFIX = 24 + + +def get_network(): + """Search adapters for the network.""" + adapters = ifaddr.get_adapters() + local_ip = get_local_ip() + network_prefix = ( + get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX + ) + return str(ip_network(f"{local_ip}/{network_prefix}", False)) + + +def get_ip_prefix_from_adapters(local_ip, adapters): + """Find the network prefix for an adapter.""" + for adapter in adapters: + for ip_cfg in adapter.ips: + if local_ip == ip_cfg.ip: + return ip_cfg.network_prefix + + +def _normalize_ips_and_network(hosts_str): + """Check if a list of hosts are all ips or ip networks.""" + + normalized_hosts = [] + hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""] + + for host in sorted(hosts): + try: + start, end = host.split("-", 1) + if "." not in end: + ip_1, ip_2, ip_3, _ = start.split(".", 3) + end = ".".join([ip_1, ip_2, ip_3, end]) + summarize_address_range(ip_address(start), ip_address(end)) + except ValueError: + pass + else: + normalized_hosts.append(host) + continue + + try: + ip_addr = ip_address(host) + except ValueError: + pass + else: + normalized_hosts.append(str(ip_addr)) + continue + + try: + network = ip_network(host) + except ValueError: + return None + else: + normalized_hosts.append(str(network)) + + return normalized_hosts + + +def normalize_input(user_input): + """Validate hosts and exclude are valid.""" + errors = {} + normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + if not normalized_hosts: + errors[CONF_HOSTS] = "invalid_hosts" + else: + user_input[CONF_HOSTS] = ",".join(normalized_hosts) + + normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE]) + if normalized_exclude is None: + errors[CONF_EXCLUDE] = "invalid_hosts" + else: + user_input[CONF_EXCLUDE] = ",".join(normalized_exclude) + + return errors + + +async def _async_build_schema_with_user_input(hass, user_input, include_options): + hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) + exclude = user_input.get( + CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) + ) + schema = { + vol.Required(CONF_HOSTS, default=hosts): str, + vol.Required( + CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) + ): int, + vol.Optional(CONF_EXCLUDE, default=exclude): str, + vol.Optional( + CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) + ): str, + } + if include_options: + schema.update( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), + ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), + } + ) + return vol.Schema(schema) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for homekit.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + errors = {} + if user_input is not None: + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.options + ) + + return self.async_show_form( + step_id="init", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options, True + ), + errors=errors, + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nmap Tracker.""" + + VERSION = 1 + + def __init__(self): + """Initialize config flow.""" + self.options = {} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", + data={}, + options=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options, False + ), + errors=errors, + ) + + def _async_is_unique_host_list(self, user_input): + hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + for entry in self._async_current_entries(): + if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts: + return False + return True + + async def async_step_import(self, user_input=None): + """Handle import from yaml.""" + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + normalize_input(user_input) + + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", data={}, options=user_input + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index 88118a81811..f8b467d2f19 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -9,8 +9,6 @@ NMAP_TRACKED_DEVICES = "nmap_tracked_devices" # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" CONF_OPTIONS = "scan_options" -DEFAULT_OPTIONS = "-F --host-timeout 5s" +DEFAULT_OPTIONS = "-F -T4 --min-rate 10 --host-timeout 5s" TRACKER_SCAN_INTERVAL = 120 - -DEFAULT_TRACK_NEW_DEVICES = True diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 69c65873e51..fcf9ae6189e 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,29 +1,35 @@ """Support for scanning a network with nmap.""" -from collections import namedtuple -from datetime import timedelta -import logging -from getmac import get_mac_address -from nmap import PortScanner, PortScannerError +import logging +from typing import Callable + import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import NmapDeviceScanner, short_hostname, signal_device_update +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) -# Interval in minutes to exclude devices from a scan while they are home -CONF_HOME_INTERVAL = "home_interval" -CONF_OPTIONS = "scan_options" -DEFAULT_OPTIONS = "-F --host-timeout 5s" - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): cv.ensure_list, @@ -34,100 +40,161 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_scanner(hass, config): +async def async_get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - return NmapDeviceScanner(config[DOMAIN]) + validated_config = config[DEVICE_TRACKER_DOMAIN] + if CONF_SCAN_INTERVAL in validated_config: + scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() + else: + scan_interval = TRACKER_SCAN_INTERVAL -Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) + import_config = { + CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), + CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], + CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), + CONF_OPTIONS: validated_config[CONF_OPTIONS], + CONF_SCAN_INTERVAL: scan_interval, + } - -class NmapDeviceScanner(DeviceScanner): - """This class scans for devices using nmap.""" - - exclude = [] - - def __init__(self, config): - """Initialize the scanner.""" - self.last_results = [] - - self.hosts = config[CONF_HOSTS] - self.exclude = config[CONF_EXCLUDE] - minutes = config[CONF_HOME_INTERVAL] - self._options = config[CONF_OPTIONS] - self.home_interval = timedelta(minutes=minutes) - - _LOGGER.debug("Scanner initialized") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - _LOGGER.debug("Nmap last results %s", self.last_results) - - return [device.mac for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - filter_named = [ - result.name for result in self.last_results if result.mac == device - ] - - if filter_named: - return filter_named[0] - return None - - def get_extra_attributes(self, device): - """Return the IP of the given device.""" - filter_ip = next( - (result.ip for result in self.last_results if result.mac == device), None + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=import_config, ) - return {"ip": filter_ip} + ) - def _update_info(self): - """Scan the network for devices. + _LOGGER.warning( + "Your Nmap Tracker configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + ) - Returns boolean if scanning successful. - """ - _LOGGER.debug("Scanning") - scanner = PortScanner() +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up device tracker for Nmap Tracker component.""" + nmap_tracker = hass.data[DOMAIN][entry.entry_id] - options = self._options + @callback + def device_new(mac_address): + """Signal a new device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)]) - if self.home_interval: - boundary = dt_util.now() - self.home_interval - last_results = [ - device for device in self.last_results if device.last_update > boundary - ] - if last_results: - exclude_hosts = self.exclude + [device.ip for device in last_results] - else: - exclude_hosts = self.exclude - else: - last_results = [] - exclude_hosts = self.exclude - if exclude_hosts: - options += f" --exclude {','.join(exclude_hosts)}" + @callback + def device_missing(mac_address): + """Signal a missing device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)]) - try: - result = scanner.scan(hosts=" ".join(self.hosts), arguments=options) - except PortScannerError: - return False + entry.async_on_unload( + async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new) + ) + entry.async_on_unload( + async_dispatcher_connect( + hass, nmap_tracker.signal_device_missing, device_missing + ) + ) - now = dt_util.now() - for ipv4, info in result["scan"].items(): - if info["status"]["state"] != "up": - continue - name = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - # Mac address only returned if nmap ran as root - mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) - if mac is None: - _LOGGER.info("No MAC address found for %s", ipv4) - continue - last_results.append(Device(mac.upper(), name, ipv4, now)) - self.last_results = last_results +class NmapTrackerEntity(ScannerEntity): + """An Nmap Tracker entity.""" - _LOGGER.debug("nmap scan successful") - return True + def __init__( + self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool + ) -> None: + """Initialize an nmap tracker entity.""" + self._mac_address = mac_address + self._nmap_tracker = nmap_tracker + self._tracked = self._nmap_tracker.devices.tracked + self._active = active + + @property + def _device(self) -> bool: + """Get latest device state.""" + return self._tracked[self._mac_address] + + @property + def is_connected(self) -> bool: + """Return device status.""" + return self._active + + @property + def name(self) -> str: + """Return device name.""" + return self._device.name + + @property + def unique_id(self) -> str: + """Return device unique id.""" + return self._mac_address + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ipv4 + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return short_hostname(self._device.hostname) + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def device_info(self): + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, + "default_manufacturer": self._device.manufacturer, + "default_name": self.name, + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def icon(self): + """Return device icon.""" + return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" + + @callback + def async_process_update(self, online: bool) -> None: + """Update device.""" + self._active = online + + @property + def extra_state_attributes(self): + """Return the attributes.""" + return { + "last_time_reachable": self._device.last_update.isoformat( + timespec="seconds" + ), + "reason": self._device.reason, + } + + @callback + def async_on_demand_update(self, online: bool): + """Update state.""" + self.async_process_update(online) + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_device_update(self._mac_address), + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 9f81c0facaf..ee05843c4fe 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -2,7 +2,13 @@ "domain": "nmap_tracker", "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [], - "iot_class": "local_polling" + "requirements": [ + "netmap==0.7.0.2", + "getmac==0.8.2", + "ifaddr==0.1.7", + "mac-vendor-lookup==0.1.11" + ], + "codeowners": ["@bdraco"], + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index ecb470a6f0d..d42e1067503 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -9,7 +9,6 @@ "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", - "track_new_devices": "Track new devices", "interval_seconds": "Scan interval" } } diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json index 1a0d0ae0b53..ac5f913d8e6 100644 --- a/homeassistant/components/nmap_tracker/translations/cs.json +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Interval skenov\u00e1n\u00ed", + "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/no.json b/homeassistant/components/nmap_tracker/translations/no.json index 487d15c910f..03a241bc3a2 100644 --- a/homeassistant/components/nmap_tracker/translations/no.json +++ b/homeassistant/components/nmap_tracker/translations/no.json @@ -28,7 +28,9 @@ "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", - "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" + "interval_seconds": "Skanneintervall", + "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap", + "track_new_devices": "Spor nye enheter" }, "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." } diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 26f7dbd2c8a..72e51837bb8 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -120,7 +120,7 @@ class NMBSLiveBoard(SensorEntity): return DEFAULT_ICON @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -166,7 +166,7 @@ class NMBSLiveBoard(SensorEntity): class NMBSSensor(SensorEntity): """Get the the total travel time for a given connection.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__( self, api_client, name, show_on_map, station_from, station_to, excl_vias @@ -238,7 +238,7 @@ class NMBSSensor(SensorEntity): return attrs @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index e637e953173..5dbee551bb7 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -107,7 +107,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity): return attr @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.data is None: return None diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 48b9a25f783..cf6c394dbda 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -63,7 +63,7 @@ class NotionSensor(NotionEntity, SensorEntity): coordinator, task_id, sensor_id, bridge_id, system_id, name, device_class ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: @@ -71,7 +71,7 @@ class NotionSensor(NotionEntity, SensorEntity): task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_TEMPERATURE: - self._attr_state = round(float(task["status"]["value"]), 1) + self._attr_native_value = round(float(task["status"]["value"]), 1) else: LOGGER.error( "Unknown task type: %s: %s", diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 52536e69027..139728a3405 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -99,7 +99,7 @@ class StationPriceSensor(CoordinatorEntity, SensorEntity): return f"{station_name} {self._fuel_type}" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if self.coordinator.data is None: return None @@ -117,7 +117,7 @@ class StationPriceSensor(CoordinatorEntity, SensorEntity): } @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the units of measurement.""" return f"{CURRENCY_CENT}/{VOLUME_LITERS}" diff --git a/homeassistant/components/nuheat/translations/hu.json b/homeassistant/components/nuheat/translations/hu.json index e6e7174e325..873b03cebff 100644 --- a/homeassistant/components/nuheat/translations/hu.json +++ b/homeassistant/components/nuheat/translations/hu.json @@ -15,7 +15,9 @@ "password": "Jelsz\u00f3", "serial_number": "A termoszt\u00e1t sorozatsz\u00e1ma.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A termoszt\u00e1t numerikus sorozatsz\u00e1m\u00e1t vagy azonos\u00edt\u00f3j\u00e1t meg kell szereznie, ha bejelentkezik a https://MyNuHeat.com oldalra, \u00e9s kiv\u00e1lasztja a termoszt\u00e1tot.", + "title": "Csatlakozzon a NuHeat-hez" } } } diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 19372de5258..fcf719c979e 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -78,12 +78,12 @@ class NumatoGpioAdc(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 0b5ad8bbc1f..70c097bd6f1 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -25,19 +25,12 @@ from .const import ( DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, - SENSOR_NAME, SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) -SENSOR_DICT = { - sensor_id: sensor_spec[SENSOR_NAME] - for sensor_id, sensor_spec in SENSOR_TYPES.items() -} - - def _base_schema(discovery_info): """Generate base schema.""" base_schema = {} @@ -59,15 +52,15 @@ def _resource_schema_base(available_resources, selected_resources): """Resource selection schema.""" known_available_resources = { - sensor_id: sensor[SENSOR_NAME] - for sensor_id, sensor in SENSOR_TYPES.items() + sensor_id: sensor_desc.name + for sensor_id, sensor_desc in SENSOR_TYPES.items() if sensor_id in available_resources } if KEY_STATUS in known_available_resources: known_available_resources[KEY_STATUS_DISPLAY] = SENSOR_TYPES[ KEY_STATUS_DISPLAY - ][SENSOR_NAME] + ].name return { vol.Required(CONF_RESOURCES, default=selected_resources): cv.multi_select( diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 1f5fecdd219..a180c2224f7 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -1,10 +1,17 @@ """The nut component.""" + +from __future__ import annotations + +from typing import Final + from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -40,246 +47,412 @@ PYNUT_MODEL = "model" PYNUT_FIRMWARE = "firmware" PYNUT_NAME = "name" -SENSOR_TYPES = { - "ups.status.display": ["Status", "", "mdi:information-outline", None], - "ups.status": ["Status Data", "", "mdi:information-outline", None], - "ups.alarm": ["Alarms", "", "mdi:alarm", None], - "ups.temperature": [ - "UPS Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "ups.load": ["Load", PERCENTAGE, "mdi:gauge", None], - "ups.load.high": ["Overload Setting", PERCENTAGE, "mdi:gauge", None], - "ups.id": ["System identifier", "", "mdi:information-outline", None], - "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer-outline", None], - "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer-outline", None], - "ups.delay.shutdown": [ - "UPS Shutdown Delay", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer-outline", None], - "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer-outline", None], - "ups.timer.shutdown": [ - "Load Shutdown Timer", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.test.interval": [ - "Self-Test Interval", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.test.result": ["Self-Test Result", "", "mdi:information-outline", None], - "ups.test.date": ["Self-Test Date", "", "mdi:calendar", None], - "ups.display.language": ["Language", "", "mdi:information-outline", None], - "ups.contacts": ["External Contacts", "", "mdi:information-outline", None], - "ups.efficiency": ["Efficiency", PERCENTAGE, "mdi:gauge", None], - "ups.power": ["Current Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None], - "ups.power.nominal": ["Nominal Power", POWER_VOLT_AMPERE, "mdi:flash", None], - "ups.realpower": [ - "Current Real Power", - POWER_WATT, - None, - DEVICE_CLASS_POWER, - ], - "ups.realpower.nominal": [ - "Nominal Real Power", - POWER_WATT, - None, - DEVICE_CLASS_POWER, - ], - "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline", None], - "ups.type": ["UPS Type", "", "mdi:information-outline", None], - "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline", None], - "ups.start.auto": ["Start on AC", "", "mdi:information-outline", None], - "ups.start.battery": ["Start on Battery", "", "mdi:information-outline", None], - "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline", None], - "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline", None], - "battery.charge": [ - "Battery Charge", - PERCENTAGE, - None, - DEVICE_CLASS_BATTERY, - ], - "battery.charge.low": ["Low Battery Setpoint", PERCENTAGE, "mdi:gauge", None], - "battery.charge.restart": [ - "Minimum Battery to Start", - PERCENTAGE, - "mdi:gauge", - None, - ], - "battery.charge.warning": [ - "Warning Battery Setpoint", - PERCENTAGE, - "mdi:gauge", - None, - ], - "battery.charger.status": ["Charging Status", "", "mdi:information-outline", None], - "battery.voltage": [ - "Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.nominal": [ - "Nominal Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.low": [ - "Low Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.high": [ - "High Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None], - "battery.current": [ - "Battery Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "battery.current.total": [ - "Total Battery Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "battery.temperature": [ - "Battery Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer-outline", None], - "battery.runtime.low": [ - "Low Battery Runtime", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "battery.runtime.restart": [ - "Minimum Battery Runtime to Start", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "battery.alarm.threshold": [ - "Battery Alarm Threshold", - "", - "mdi:information-outline", - None, - ], - "battery.date": ["Battery Date", "", "mdi:calendar", None], - "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar", None], - "battery.packs": ["Number of Batteries", "", "mdi:information-outline", None], - "battery.packs.bad": [ - "Number of Bad Batteries", - "", - "mdi:information-outline", - None, - ], - "battery.type": ["Battery Chemistry", "", "mdi:information-outline", None], - "input.sensitivity": [ - "Input Power Sensitivity", - "", - "mdi:information-outline", - None, - ], - "input.transfer.low": [ - "Low Voltage Transfer", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.transfer.high": [ - "High Voltage Transfer", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.transfer.reason": [ - "Voltage Transfer Reason", - "", - "mdi:information-outline", - None, - ], - "input.voltage": [ - "Input Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.voltage.nominal": [ - "Nominal Input Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.frequency": ["Input Line Frequency", FREQUENCY_HERTZ, "mdi:flash", None], - "input.frequency.nominal": [ - "Nominal Input Line Frequency", - FREQUENCY_HERTZ, - "mdi:flash", - None, - ], - "input.frequency.status": [ - "Input Frequency Status", - "", - "mdi:information-outline", - None, - ], - "output.current": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None], - "output.current.nominal": [ - "Nominal Output Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "output.voltage": [ - "Output Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "output.voltage.nominal": [ - "Nominal Output Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "output.frequency": ["Output Frequency", FREQUENCY_HERTZ, "mdi:flash", None], - "output.frequency.nominal": [ - "Nominal Output Frequency", - FREQUENCY_HERTZ, - "mdi:flash", - None, - ], - "ambient.humidity": [ - "Ambient Humidity", - PERCENTAGE, - None, - DEVICE_CLASS_HUMIDITY, - ], - "ambient.temperature": [ - "Ambient Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], +SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { + "ups.status.display": SensorEntityDescription( + key="ups.status.display", + name="Status", + icon="mdi:information-outline", + ), + "ups.status": SensorEntityDescription( + key="ups.status", + name="Status Data", + icon="mdi:information-outline", + ), + "ups.alarm": SensorEntityDescription( + key="ups.alarm", + name="Alarms", + icon="mdi:alarm", + ), + "ups.temperature": SensorEntityDescription( + key="ups.temperature", + name="UPS Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.load": SensorEntityDescription( + key="ups.load", + name="Load", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.load.high": SensorEntityDescription( + key="ups.load.high", + name="Overload Setting", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + ), + "ups.id": SensorEntityDescription( + key="ups.id", + name="System identifier", + icon="mdi:information-outline", + ), + "ups.delay.start": SensorEntityDescription( + key="ups.delay.start", + name="Load Restart Delay", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.delay.reboot": SensorEntityDescription( + key="ups.delay.reboot", + name="UPS Reboot Delay", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.delay.shutdown": SensorEntityDescription( + key="ups.delay.shutdown", + name="UPS Shutdown Delay", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.timer.start": SensorEntityDescription( + key="ups.timer.start", + name="Load Start Timer", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.timer.reboot": SensorEntityDescription( + key="ups.timer.reboot", + name="Load Reboot Timer", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.timer.shutdown": SensorEntityDescription( + key="ups.timer.shutdown", + name="Load Shutdown Timer", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.test.interval": SensorEntityDescription( + key="ups.test.interval", + name="Self-Test Interval", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "ups.test.result": SensorEntityDescription( + key="ups.test.result", + name="Self-Test Result", + icon="mdi:information-outline", + ), + "ups.test.date": SensorEntityDescription( + key="ups.test.date", + name="Self-Test Date", + icon="mdi:calendar", + ), + "ups.display.language": SensorEntityDescription( + key="ups.display.language", + name="Language", + icon="mdi:information-outline", + ), + "ups.contacts": SensorEntityDescription( + key="ups.contacts", + name="External Contacts", + icon="mdi:information-outline", + ), + "ups.efficiency": SensorEntityDescription( + key="ups.efficiency", + name="Efficiency", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.power": SensorEntityDescription( + key="ups.power", + name="Current Apparent Power", + native_unit_of_measurement=POWER_VOLT_AMPERE, + icon="mdi:flash", + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.power.nominal": SensorEntityDescription( + key="ups.power.nominal", + name="Nominal Power", + native_unit_of_measurement=POWER_VOLT_AMPERE, + icon="mdi:flash", + ), + "ups.realpower": SensorEntityDescription( + key="ups.realpower", + name="Current Real Power", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.realpower.nominal": SensorEntityDescription( + key="ups.realpower.nominal", + name="Nominal Real Power", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + "ups.beeper.status": SensorEntityDescription( + key="ups.beeper.status", + name="Beeper Status", + icon="mdi:information-outline", + ), + "ups.type": SensorEntityDescription( + key="ups.type", + name="UPS Type", + icon="mdi:information-outline", + ), + "ups.watchdog.status": SensorEntityDescription( + key="ups.watchdog.status", + name="Watchdog Status", + icon="mdi:information-outline", + ), + "ups.start.auto": SensorEntityDescription( + key="ups.start.auto", + name="Start on AC", + icon="mdi:information-outline", + ), + "ups.start.battery": SensorEntityDescription( + key="ups.start.battery", + name="Start on Battery", + icon="mdi:information-outline", + ), + "ups.start.reboot": SensorEntityDescription( + key="ups.start.reboot", + name="Reboot on Battery", + icon="mdi:information-outline", + ), + "ups.shutdown": SensorEntityDescription( + key="ups.shutdown", + name="Shutdown Ability", + icon="mdi:information-outline", + ), + "battery.charge": SensorEntityDescription( + key="battery.charge", + name="Battery Charge", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.charge.low": SensorEntityDescription( + key="battery.charge.low", + name="Low Battery Setpoint", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + ), + "battery.charge.restart": SensorEntityDescription( + key="battery.charge.restart", + name="Minimum Battery to Start", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + ), + "battery.charge.warning": SensorEntityDescription( + key="battery.charge.warning", + name="Warning Battery Setpoint", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + ), + "battery.charger.status": SensorEntityDescription( + key="battery.charger.status", + name="Charging Status", + icon="mdi:information-outline", + ), + "battery.voltage": SensorEntityDescription( + key="battery.voltage", + name="Battery Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.voltage.nominal": SensorEntityDescription( + key="battery.voltage.nominal", + name="Nominal Battery Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "battery.voltage.low": SensorEntityDescription( + key="battery.voltage.low", + name="Low Battery Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "battery.voltage.high": SensorEntityDescription( + key="battery.voltage.high", + name="High Battery Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "battery.capacity": SensorEntityDescription( + key="battery.capacity", + name="Battery Capacity", + native_unit_of_measurement="Ah", + icon="mdi:flash", + ), + "battery.current": SensorEntityDescription( + key="battery.current", + name="Battery Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.current.total": SensorEntityDescription( + key="battery.current.total", + name="Total Battery Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + ), + "battery.temperature": SensorEntityDescription( + key="battery.temperature", + name="Battery Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.runtime": SensorEntityDescription( + key="battery.runtime", + name="Battery Runtime", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "battery.runtime.low": SensorEntityDescription( + key="battery.runtime.low", + name="Low Battery Runtime", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "battery.runtime.restart": SensorEntityDescription( + key="battery.runtime.restart", + name="Minimum Battery Runtime to Start", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + "battery.alarm.threshold": SensorEntityDescription( + key="battery.alarm.threshold", + name="Battery Alarm Threshold", + icon="mdi:information-outline", + ), + "battery.date": SensorEntityDescription( + key="battery.date", + name="Battery Date", + icon="mdi:calendar", + ), + "battery.mfr.date": SensorEntityDescription( + key="battery.mfr.date", + name="Battery Manuf. Date", + icon="mdi:calendar", + ), + "battery.packs": SensorEntityDescription( + key="battery.packs", + name="Number of Batteries", + icon="mdi:information-outline", + ), + "battery.packs.bad": SensorEntityDescription( + key="battery.packs.bad", + name="Number of Bad Batteries", + icon="mdi:information-outline", + ), + "battery.type": SensorEntityDescription( + key="battery.type", + name="Battery Chemistry", + icon="mdi:information-outline", + ), + "input.sensitivity": SensorEntityDescription( + key="input.sensitivity", + name="Input Power Sensitivity", + icon="mdi:information-outline", + ), + "input.transfer.low": SensorEntityDescription( + key="input.transfer.low", + name="Low Voltage Transfer", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "input.transfer.high": SensorEntityDescription( + key="input.transfer.high", + name="High Voltage Transfer", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "input.transfer.reason": SensorEntityDescription( + key="input.transfer.reason", + name="Voltage Transfer Reason", + icon="mdi:information-outline", + ), + "input.voltage": SensorEntityDescription( + key="input.voltage", + name="Input Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "input.voltage.nominal": SensorEntityDescription( + key="input.voltage.nominal", + name="Nominal Input Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "input.frequency": SensorEntityDescription( + key="input.frequency", + name="Input Line Frequency", + native_unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + state_class=STATE_CLASS_MEASUREMENT, + ), + "input.frequency.nominal": SensorEntityDescription( + key="input.frequency.nominal", + name="Nominal Input Line Frequency", + native_unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + ), + "input.frequency.status": SensorEntityDescription( + key="input.frequency.status", + name="Input Frequency Status", + icon="mdi:information-outline", + ), + "output.current": SensorEntityDescription( + key="output.current", + name="Output Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.current.nominal": SensorEntityDescription( + key="output.current.nominal", + name="Nominal Output Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + ), + "output.voltage": SensorEntityDescription( + key="output.voltage", + name="Output Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.voltage.nominal": SensorEntityDescription( + key="output.voltage.nominal", + name="Nominal Output Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + ), + "output.frequency": SensorEntityDescription( + key="output.frequency", + name="Output Frequency", + native_unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.frequency.nominal": SensorEntityDescription( + key="output.frequency.nominal", + name="Nominal Output Frequency", + native_unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + ), + "ambient.humidity": SensorEntityDescription( + key="ambient.humidity", + name="Ambient Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ambient.temperature": SensorEntityDescription( + key="ambient.temperature", + name="Ambient Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), } STATE_TYPES = { @@ -299,8 +472,3 @@ STATE_TYPES = { "FSD": "Forced Shutdown", "ALARM": "Alarm", } - -SENSOR_NAME = 0 -SENSOR_UNIT = 1 -SENSOR_ICON = 2 -SENSOR_DEVICE_CLASS = 3 diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 1eb67e45aa5..995032eb0fd 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,9 +1,15 @@ """Provides a sensor to track various status aspects of a UPS.""" +from __future__ import annotations + import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.nut import PyNUTData +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ATTR_STATE, CONF_RESOURCES, STATE_UNKNOWN -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( COORDINATOR, @@ -16,11 +22,7 @@ from .const import ( PYNUT_MODEL, PYNUT_NAME, PYNUT_UNIQUE_ID, - SENSOR_DEVICE_CLASS, - SENSOR_ICON, - SENSOR_NAME, SENSOR_TYPES, - SENSOR_UNIT, STATE_TYPES, ) @@ -60,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator, data, name.title(), - sensor_type, + SENSOR_TYPES[sensor_type], unique_id, manufacturer, model, @@ -82,18 +84,18 @@ class NUTSensor(CoordinatorEntity, SensorEntity): def __init__( self, - coordinator, - data, - name, - sensor_type, - unique_id, - manufacturer, - model, - firmware, - ): + coordinator: DataUpdateCoordinator, + data: PyNUTData, + name: str, + sensor_description: SensorEntityDescription, + unique_id: str, + manufacturer: str | None, + model: str | None, + firmware: str | None, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._type = sensor_type + self.entity_description = sensor_description self._manufacturer = manufacturer self._firmware = firmware self._model = model @@ -101,10 +103,9 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._data = data self._unique_id = unique_id - self._attr_device_class = SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] - self._attr_icon = SENSOR_TYPES[self._type][SENSOR_ICON] - self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][SENSOR_UNIT] + self._attr_name = f"{name} {sensor_description.name}" + if unique_id is not None: + self._attr_unique_id = f"{unique_id}_{sensor_description.key}" @property def device_info(self): @@ -124,20 +125,13 @@ class NUTSensor(CoordinatorEntity, SensorEntity): return device_info @property - def unique_id(self): - """Sensor Unique id.""" - if not self._unique_id: - return None - return f"{self._unique_id}_{self._type}" - - @property - def state(self): + def native_value(self): """Return entity state from ups.""" if not self._data.status: return None - if self._type == KEY_STATUS_DISPLAY: + if self.entity_description.key == KEY_STATUS_DISPLAY: return _format_display_state(self._data.status) - return self._data.status.get(self._type) + return self._data.status.get(self.entity_description.key) @property def extra_state_attributes(self): diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json index a7bad455dc3..bfc8e01c11a 100644 --- a/homeassistant/components/nut/translations/hu.json +++ b/homeassistant/components/nut/translations/hu.json @@ -8,13 +8,27 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "resources": { + "data": { + "resources": "Forr\u00e1sok" + }, + "title": "V\u00e1lassza ki a nyomon k\u00f6vetend\u0151 er\u0151forr\u00e1sokat" + }, + "ups": { + "data": { + "alias": "\u00c1ln\u00e9v", + "resources": "Forr\u00e1sok" + }, + "title": "V\u00e1lassza ki a fel\u00fcgyelni k\u00edv\u00e1nt UPS-t" + }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a NUT szerverhez" } } }, @@ -22,6 +36,15 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "resources": "Forr\u00e1sok", + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "V\u00e1lassza az \u00c9rz\u00e9kel\u0151 er\u0151forr\u00e1sokat." + } } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/zh-Hans.json b/homeassistant/components/nut/translations/zh-Hans.json index 91522c7f609..4afd1ff0031 100644 --- a/homeassistant/components/nut/translations/zh-Hans.json +++ b/homeassistant/components/nut/translations/zh-Hans.json @@ -1,15 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, "step": { "resources": { "data": { "resources": "\u8d44\u6e90" - } + }, + "title": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" + }, + "ups": { + "data": { + "alias": "\u522b\u540d", + "resources": "\u8d44\u6e90" + }, + "title": "\u9009\u62e9\u8981\u76d1\u63a7\u7684 UPS" }, "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" - } + }, + "title": "\u8fde\u63a5\u5230 NUT \u670d\u52a1\u5668" } } }, @@ -17,6 +36,15 @@ "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "unknown": "\u4e0d\u5728\u9884\u671f\u5185\u7684\u9519\u8bef" + }, + "step": { + "init": { + "data": { + "resources": "\u8d44\u6e90", + "scan_interval": "\u626b\u63cf\u95f4\u9694\uff08\u79d2\uff09" + }, + "description": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 6e08ef408d3..32018bc40bb 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -113,7 +113,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Dew Point", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -121,7 +121,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Temperature", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -129,7 +129,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Chill", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -137,7 +137,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Heat Index", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -145,7 +145,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Relative Humidity", icon=None, device_class=DEVICE_CLASS_HUMIDITY, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, unit_convert=PERCENTAGE, ), NWSSensorEntityDescription( @@ -153,7 +153,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Speed", icon="mdi:weather-windy", device_class=None, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), NWSSensorEntityDescription( @@ -161,7 +161,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Gust", icon="mdi:weather-windy", device_class=None, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), NWSSensorEntityDescription( @@ -169,7 +169,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Direction", icon="mdi:compass-rose", device_class=None, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, unit_convert=DEGREE, ), NWSSensorEntityDescription( @@ -177,7 +177,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Barometric Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit_of_measurement=PRESSURE_PA, + native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), NWSSensorEntityDescription( @@ -185,7 +185,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Sea Level Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit_of_measurement=PRESSURE_PA, + native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), NWSSensorEntityDescription( @@ -193,7 +193,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Visibility", icon="mdi:eye", device_class=None, - unit_of_measurement=LENGTH_METERS, + native_unit_of_measurement=LENGTH_METERS, unit_convert=LENGTH_MILES, ), ) diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 409856831a2..85b60ffd475 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -73,16 +73,16 @@ class NWSSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{station} {description.name}" if not hass.config.units.is_metric: - self._attr_unit_of_measurement = description.unit_convert + self._attr_native_unit_of_measurement = description.unit_convert @property - def state(self): + def native_value(self): """Return the state.""" value = self._nws.observation.get(self.entity_description.key) if value is None: return None # Set alias to unit property -> prevent unnecessary hasattr calls - unit_of_measurement = self.unit_of_measurement + unit_of_measurement = self.native_unit_of_measurement if unit_of_measurement == SPEED_MILES_PER_HOUR: return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) if unit_of_measurement == LENGTH_MILES: diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json index 1d674cacc7e..ec9bf3f4988 100644 --- a/homeassistant/components/nws/translations/hu.json +++ b/homeassistant/components/nws/translations/hu.json @@ -12,8 +12,11 @@ "data": { "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - } + "longitude": "Hossz\u00fas\u00e1g", + "station": "METAR \u00e1llom\u00e1s k\u00f3dja" + }, + "description": "Ha a METAR \u00e1llom\u00e1s k\u00f3dja nincs megadva, a sz\u00e9less\u00e9gi \u00e9s hossz\u00fas\u00e1gi fokokat haszn\u00e1lja a legk\u00f6zelebbi \u00e1llom\u00e1s megkeres\u00e9s\u00e9hez. Egyel\u0151re az API-kulcs b\u00e1rmi lehet. Javasoljuk, hogy \u00e9rv\u00e9nyes e -mail c\u00edmet haszn\u00e1ljon.", + "title": "Csatlakozzon az National Weather Service-hez" } } } diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 71f885ce491..ebb3a7e4e66 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -60,7 +61,7 @@ SPEED_LIMIT_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NZBGet integration.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 97bced9e9c2..325438908a7 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -103,12 +103,12 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): return self._unique_id @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit that the state of sensor is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = self.coordinator.data["status"].get(self._sensor_type) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 71af8dacba2..4c9b583a36b 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -73,7 +73,7 @@ class OASATelematicsSensor(SensorEntity): return DEVICE_CLASS_TIMESTAMP @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 639b9eb332f..7ba28ee0741 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -85,7 +85,7 @@ class ObihaiServiceSensors(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 16f6efce004..5b2b0af494c 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -107,7 +107,7 @@ class OctoPrintSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" sensor_unit = self.unit_of_measurement if sensor_unit in (TEMP_CELSIUS, PERCENTAGE): @@ -118,7 +118,7 @@ class OctoPrintSensor(SensorEntity): return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index b53c35e17b5..0638b32d105 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -48,7 +48,7 @@ class OhmconnectSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._data.get("active") == "True": return "Active" diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index c91cf429c94..50bb121dc4b 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -53,7 +53,7 @@ class OmbiSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 1f8de082868..f0382c01342 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -86,7 +86,7 @@ class OmnilogicSensor(OmniLogicEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the right unit of measure.""" return self._unit @@ -95,7 +95,7 @@ class OmniLogicTemperatureSensor(OmnilogicSensor): """Define an OmniLogic Temperature (Air/Water) Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the temperature sensor.""" sensor_data = self.coordinator.data[self._item_id][self._state_key] @@ -123,7 +123,7 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): """Define an OmniLogic Pump Speed Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the pump speed sensor.""" pump_type = PUMP_TYPES[ @@ -158,7 +158,7 @@ class OmniLogicSaltLevelSensor(OmnilogicSensor): """Define an OmniLogic Salt Level Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the salt level sensor.""" salt_return = self.coordinator.data[self._item_id][self._state_key] @@ -177,7 +177,7 @@ class OmniLogicChlorinatorSensor(OmnilogicSensor): """Define an OmniLogic Chlorinator Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the chlorinator sensor.""" state = self.coordinator.data[self._item_id][self._state_key] @@ -188,7 +188,7 @@ class OmniLogicPHSensor(OmnilogicSensor): """Define an OmniLogic pH Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the pH sensor.""" ph_state = self.coordinator.data[self._item_id][self._state_key] @@ -232,7 +232,7 @@ class OmniLogicORPSensor(OmnilogicSensor): ) @property - def state(self): + def native_value(self): """Return the state for the ORP sensor.""" orp_state = int(self.coordinator.data[self._item_id][self._state_key]) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 26a61ddfe4c..693d685f77c 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -28,49 +28,49 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, ), SensorEntityDescription( key="orp", name="Oxydo Reduction Potential", - unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="ph", name="pH", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="tds", name="TDS", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="battery", name="Battery", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_BATTERY, ), SensorEntityDescription( key="rssi", name="RSSI", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), SensorEntityDescription( key="salt", name="Salt", - unit_of_measurement="mg/L", + native_unit_of_measurement="mg/L", icon="mdi:pool", device_class=None, ), @@ -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.""" @@ -164,15 +164,10 @@ class OndiloICO(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """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.""" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 024d540c10a..215ba6c569b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -368,7 +368,7 @@ class OneWireSensor(OneWireBaseEntity, SensorEntity): """Mixin for sensor specific attributes.""" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -377,7 +377,7 @@ class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): """Implementation of a 1-Wire sensor connected through owserver.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" return self._state @@ -405,7 +405,7 @@ class OneWireDirectSensor(OneWireSensor): self._owsensor = owsensor @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 5c44cdf1750..67bec21e123 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_per_platform +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_RTSP_TRANSPORT, @@ -31,7 +32,7 @@ from .const import ( from .device import ONVIFDevice -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ONVIF component.""" # Import from yaml configs = {} diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0e95d24ef78..bb7cffa86f9 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,12 +1,12 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" -import asyncio +from __future__ import annotations from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame from onvif.exceptions import ONVIFError import voluptuous as vol from yarl import URL +from homeassistant.components import ffmpeg from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG from homeassistant.const import HTTP_BASIC_AUTHENTICATION @@ -120,7 +120,9 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Return the stream source.""" return self._stream_uri - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" image = None @@ -137,15 +139,12 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: - ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary) - image = await asyncio.shield( - ffmpeg.get_image( - self._stream_uri, - output_format=IMAGE_JPEG, - extra_cmd=self.device.config_entry.options.get( - CONF_EXTRA_ARGUMENTS - ), - ) + return await ffmpeg.async_get_image( + self.hass, + self._stream_uri, + extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), + width=width, + height=height, ) return image diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 1c5766e3969..5c31644ba19 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -44,7 +44,7 @@ class ONVIFSensor(ONVIFBaseEntity, SensorEntity): super().__init__(device) @property - def state(self) -> None | str | int | float: + def native_value(self) -> None | str | int | float: """Return the state of the entity.""" return self.device.events.get_uid(self.uid).value @@ -59,7 +59,7 @@ class ONVIFSensor(ONVIFBaseEntity, SensorEntity): return self.device.events.get_uid(self.uid).device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.device.events.get_uid(self.uid).unit_of_measurement diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index e2b63a6c9d8..c43df53ae9f 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -53,7 +53,19 @@ "data": { "auto": "Automatikus keres\u00e9s" }, - "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban." + "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban.", + "title": "ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG opci\u00f3k", + "rtsp_transport": "RTSP sz\u00e1ll\u00edt\u00e1si mechanizmus" + }, + "title": "ONVIF eszk\u00f6z opci\u00f3i" } } } diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json index 0a0b6db3d38..8ebde5a1bda 100644 --- a/homeassistant/components/onvif/translations/zh-Hans.json +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -1,19 +1,69 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d", + "no_h264": "\u65e0\u53ef\u7528\u7684 H264 \u76f4\u64ad\u6d41\u3002\u8bf7\u68c0\u67e5\u8be5\u8bbe\u5907\u4e0a\u7684\u914d\u7f6e\u6587\u4ef6\u3002", + "no_mac": "\u65e0\u6cd5\u4e3a ONVIF \u914d\u7f6e\u8bbe\u5907\u552f\u4e00 ID", + "onvif_error": "\u914d\u7f6e ONVIF \u8bbe\u5907\u65f6\u51fa\u9519\u3002\u68c0\u67e5\u65e5\u5fd7\u4ee5\u83b7\u53d6\u66f4\u591a\u4fe1\u606f\u3002" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "auth": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" - } + }, + "title": "\u914d\u7f6e\u8ba4\u8bc1\u4fe1\u606f" + }, + "configure": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + }, + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" + }, + "configure_profile": { + "data": { + "include": "\u521b\u5efa\u6444\u50cf\u673a\u5b9e\u4f53" + }, + "description": "\u4ee5 {resolution} \u5206\u8fa8\u7387\u521b\u5efa {profile} \u6444\u50cf\u673a\u5b9e\u4f53\uff1f", + "title": "\u914d\u7f6e \u914d\u7f6e\u6587\u4ef6" + }, + "device": { + "data": { + "host": "\u9009\u62e9\u5df2\u88ab\u53d1\u73b0\u7684 ONVIF \u8bbe\u5907" + }, + "title": "\u9009\u62e9 ONVIF \u8bbe\u5907" + }, + "manual_input": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0", + "port": "\u7aef\u53e3" + }, + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" + }, + "user": { + "data": { + "auto": "\u81ea\u52a8\u641c\u7d22" + }, + "description": "\u901a\u8fc7\u70b9\u51fb\u63d0\u4ea4\u6309\u94ae\uff0cHome Assistant \u5c06\u4f1a\u5c1d\u8bd5\u641c\u7d22\u60a8\u7684\u7f51\u7edc\u4e2d\u652f\u6301 Profile S \u7684 ONVIF \u8bbe\u5907\u3002\n\n\u9700\u8981\u6ce8\u610f\u7684\u662f\uff0c\u6709\u4e9b\u751f\u4ea7\u5546\u51fa\u5382\u65f6\u9ed8\u8ba4\u4f1a\u5c06 ONVIF \u529f\u80fd\u5173\u95ed\u3002\u8bf7\u786e\u8ba4\u60a8\u7684\u6444\u50cf\u5934\u5df2\u6253\u5f00\u8be5\u529f\u80fd\u3002", + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" } } }, "options": { "step": { "onvif_devices": { + "data": { + "extra_arguments": "\u9644\u52a0 FFmpeg \u53c2\u6570", + "rtsp_transport": "RTSP \u4f20\u8f93\u901a\u8baf\u534f\u8bae" + }, "title": "ONVIF \u8bbe\u5907\u9009\u9879" } } diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 33305b677de..8a3c2c0460d 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -44,7 +44,7 @@ class OpenERZSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 29eeceb232c..a1920e145bb 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -70,12 +70,12 @@ class OpenEVSESensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 8474cdab131..803123a88c3 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -73,7 +73,7 @@ class OpenexchangeratesSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 8f43c1e5e9b..280acab5d0e 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -62,12 +62,12 @@ class OpenHardwareMonitorDevice(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.value diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 122388b85b7..0502d6c6573 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -107,7 +107,7 @@ class OpenSkySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -178,7 +178,7 @@ class OpenSkySensor(SensorEntity): return {ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION} @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "flights" diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1d9904ea59f..28f139f188f 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -156,12 +156,12 @@ class OpenThermSensor(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 77112bd8929..3127dc523ce 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -24,7 +24,8 @@ "read_precision": "Pontoss\u00e1g olvas\u00e1sa", "set_precision": "Pontoss\u00e1g be\u00e1ll\u00edt\u00e1sa", "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" - } + }, + "description": "Opci\u00f3k az OpenTherm \u00e1tj\u00e1r\u00f3hoz" } } } 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..d8652ae09c5 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -51,10 +51,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -71,7 +67,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/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 386527ebc3e..e115f9294a5 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -105,7 +105,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_icon = icon self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def update_from_latest_data(self) -> None: @@ -119,22 +119,22 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_available = True if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL: - self._attr_state = data["ozone"] + self._attr_native_value = data["ozone"] elif self._sensor_type == TYPE_CURRENT_UV_INDEX: - self._attr_state = data["uv"] + self._attr_native_value = data["uv"] elif self._sensor_type == TYPE_CURRENT_UV_LEVEL: if data["uv"] >= 11: - self._attr_state = UV_LEVEL_EXTREME + self._attr_native_value = UV_LEVEL_EXTREME elif data["uv"] >= 8: - self._attr_state = UV_LEVEL_VHIGH + self._attr_native_value = UV_LEVEL_VHIGH elif data["uv"] >= 6: - self._attr_state = UV_LEVEL_HIGH + self._attr_native_value = UV_LEVEL_HIGH elif data["uv"] >= 3: - self._attr_state = UV_LEVEL_MODERATE + self._attr_native_value = UV_LEVEL_MODERATE else: - self._attr_state = UV_LEVEL_LOW + self._attr_native_value = UV_LEVEL_LOW elif self._sensor_type == TYPE_MAX_UV_INDEX: - self._attr_state = data["uv_max"] + self._attr_native_value = data["uv_max"] uv_max_time = parse_datetime(data["uv_max_time"]) if uv_max_time: self._attr_extra_state_attributes.update( @@ -148,6 +148,6 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, ): - self._attr_state = data["safe_exposure_time"][ + self._attr_native_value = data["safe_exposure_time"][ EXPOSURE_TYPE_MAP[self._sensor_type] ] diff --git a/homeassistant/components/openuv/translations/lt.json b/homeassistant/components/openuv/translations/lt.json new file mode 100644 index 00000000000..1546651d54a --- /dev/null +++ b/homeassistant/components/openuv/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neteisingas API raktas" + }, + "step": { + "user": { + "data": { + "api_key": "API raktas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index ea12123b707..3c66ca50f3c 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -71,7 +71,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 39c50c3b941..3586f958a6a 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -68,7 +68,7 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._weather_coordinator.data.get(self._sensor_type, None) @@ -91,7 +91,7 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) if forecasts is not None and len(forecasts) > 0: diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index c17873aefea..dc2b7216534 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -42,7 +42,7 @@ class CurrentEnergyUsageSensor(SensorEntity): """Representation of the sensor.""" _attr_icon = SENSOR_ICON - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, meter): """Initialize the sensor.""" @@ -61,7 +61,7 @@ class CurrentEnergyUsageSensor(SensorEntity): return SENSOR_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 7aee9d99208..45bedb6b499 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -63,7 +63,7 @@ class TOTPSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 7615a7011d3..e1130ca36a5 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -1,11 +1,14 @@ """Support for OVO Energy sensors.""" +from __future__ import annotations + from datetime import timedelta from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_ENERGY, DEVICE_CLASS_MONETARY from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -57,6 +60,8 @@ async def async_setup_entry( class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): """Defines a OVO Energy sensor.""" + _attr_state_class = STATE_CLASS_TOTAL_INCREASING + def __init__( self, coordinator: DataUpdateCoordinator, @@ -64,15 +69,17 @@ class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): key: str, name: str, icon: str, - unit_of_measurement: str = "", + device_class: str | None, + unit_of_measurement: str | None, ) -> None: """Initialize OVO Energy sensor.""" + self._attr_device_class = device_class self._unit_of_measurement = unit_of_measurement super().__init__(coordinator, client, key, name, icon) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -89,11 +96,12 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): f"{client.account_id}_last_electricity_reading", "OVO Last Electricity Reading", "mdi:flash", + DEVICE_CLASS_ENERGY, "kWh", ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -124,11 +132,12 @@ class OVOEnergyLastGasReading(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_gas_reading", "OVO Last Gas Reading", "mdi:gas-cylinder", + DEVICE_CLASS_ENERGY, "kWh", ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: @@ -160,11 +169,12 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_electricity_cost", "OVO Last Electricity Cost", "mdi:cash-multiple", + DEVICE_CLASS_MONETARY, currency, ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -196,11 +206,12 @@ class OVOEnergyLastGasCost(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_gas_cost", "OVO Last Gas Cost", "mdi:cash-multiple", + DEVICE_CLASS_MONETARY, currency, ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 143d1a8dc18..2c794b5cd9d 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -19,6 +19,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "\u00c1ll\u00edtson be egy OVO Energy p\u00e9ld\u00e1nyt az energiafelhaszn\u00e1l\u00e1s el\u00e9r\u00e9s\u00e9hez.", "title": "OVO Energy azonos\u00edt\u00f3 megad\u00e1sa" } } diff --git a/homeassistant/components/ovo_energy/translations/zh-Hans.json b/homeassistant/components/ovo_energy/translations/zh-Hans.json index a7477e8c370..cf7a799531d 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hans.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hans.json @@ -4,9 +4,16 @@ "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_auth": "\u9a8c\u8bc1\u7801\u9519\u8bef" }, + "flow_title": "{username}", "step": { + "reauth": { + "data": { + "password": "\u5bc6\u7801" + } + }, "user": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" } } diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index 0ff08a87d16..97b7b01d4d4 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -106,12 +106,12 @@ class ZWaveStringSensor(ZwaveSensorBase): """Representation of a Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return state of the sensor.""" return self.values.primary.value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement the value is expressed in.""" return self.values.primary.units @@ -125,12 +125,12 @@ class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return state of the sensor.""" return round(self.values.primary.value, 2) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement the value is expressed in.""" if self.values.primary.units == "C": return TEMP_CELSIUS @@ -144,7 +144,7 @@ class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave list sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" # We use the id as value for backwards compatibility return self.values.primary.value["Selected_id"] diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 70934bf3472..a43f234c909 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -6,6 +6,7 @@ "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index e187b7c18a5..ab63b535e80 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 HTTPError, URLError +from urllib.error import HTTPError, URLError from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 42400e7348c..d1c6461de21 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Panasonic Viera TV integration.""" from functools import partial import logging -from urllib.request import URLError +from urllib.error import URLError from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError import voluptuous as vol diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index cfc0be387d0..df520bb1ca5 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -22,7 +22,8 @@ "host": "IP c\u00edm", "name": "N\u00e9v" }, - "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet" + "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", + "title": "A TV be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 4a68dd3356f..ec2c5f7512d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -15,6 +15,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -100,7 +101,7 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the persistent notification component.""" persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() hass.data[DOMAIN] = {"notifications": persistent_notifications} diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 7641a75e9c6..ba1f0ced623 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -293,7 +293,7 @@ The following persons point at invalid users: return filtered -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the person component.""" entity_component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 4f3ee5a9ab3..3bea3ff7337 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.7.4"], + "requirements": ["ha-philipsjs==2.7.5"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ab9191b0f4a..5c679a4839a 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -174,11 +174,6 @@ class PiHoleEntity(CoordinatorEntity): self._name = name self._server_unique_id = server_unique_id - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return "mdi:pi-hole" - @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 3c322d324d3..5758c0e4145 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -29,6 +29,8 @@ async def async_setup_entry( class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity): """Representation of a Pi-hole binary sensor.""" + _attr_icon = "mdi:pi-hole" + @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index f1871bf27c8..f1ec1c6efd6 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,6 +1,10 @@ """Constants for the pi_hole integration.""" +from __future__ import annotations + +from dataclasses import dataclass from datetime import timedelta +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" @@ -25,28 +29,67 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" DATA_KEY_COORDINATOR = "coordinator" -SENSOR_DICT = { - "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], - "ads_percentage_today": [ - "Ads Percentage Blocked Today", - PERCENTAGE, - "mdi:close-octagon-outline", - ], - "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], - "dns_queries_today": [ - "DNS Queries Today", - "queries", - "mdi:comment-question-outline", - ], - "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], - "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], - "queries_forwarded": [ - "DNS Queries Forwarded", - "queries", - "mdi:comment-question-outline", - ], - "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], - "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], -} -SENSOR_LIST = list(SENSOR_DICT) +@dataclass +class PiHoleSensorEntityDescription(SensorEntityDescription): + """Describes PiHole sensor entity.""" + + icon: str = "mdi:pi-hole" + + +SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( + PiHoleSensorEntityDescription( + key="ads_blocked_today", + name="Ads Blocked Today", + native_unit_of_measurement="ads", + icon="mdi:close-octagon-outline", + ), + PiHoleSensorEntityDescription( + key="ads_percentage_today", + name="Ads Percentage Blocked Today", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:close-octagon-outline", + ), + PiHoleSensorEntityDescription( + key="clients_ever_seen", + name="Seen Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + PiHoleSensorEntityDescription( + key="dns_queries_today", + name="DNS Queries Today", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="domains_being_blocked", + name="Domains Blocked", + native_unit_of_measurement="domains", + icon="mdi:block-helper", + ), + PiHoleSensorEntityDescription( + key="queries_cached", + name="DNS Queries Cached", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="queries_forwarded", + name="DNS Queries Forwarded", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="unique_clients", + name="DNS Unique Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + PiHoleSensorEntityDescription( + key="unique_domains", + name="DNS Unique Domains", + native_unit_of_measurement="domains", + icon="mdi:domain", + ), +) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 95aee56f7cc..0e231868647 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -18,8 +18,8 @@ from .const import ( DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, - SENSOR_DICT, - SENSOR_LIST, + SENSOR_TYPES, + PiHoleSensorEntityDescription, ) @@ -34,10 +34,10 @@ async def async_setup_entry( hole_data[DATA_KEY_API], hole_data[DATA_KEY_COORDINATOR], name, - sensor_name, entry.entry_id, + description, ) - for sensor_name in SENSOR_LIST + for description in SENSOR_TYPES ] async_add_entities(sensors, True) @@ -45,51 +45,30 @@ async def async_setup_entry( class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" + entity_description: PiHoleSensorEntityDescription + def __init__( self, api: Hole, coordinator: DataUpdateCoordinator, name: str, - sensor_name: str, server_unique_id: str, + description: PiHoleSensorEntityDescription, ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) + self.entity_description = description - self._condition = sensor_name - - variable_info = SENSOR_DICT[sensor_name] - self._condition_name = variable_info[0] - self._unit_of_measurement = variable_info[1] - self._icon = variable_info[2] + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{self._server_unique_id}/{description.name}" @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._name} {self._condition_name}" - - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return f"{self._server_unique_id}/{self._condition_name}" - - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def state(self) -> Any: + def native_value(self) -> Any: """Return the state of the device.""" try: - return round(self.api.data[self._condition], 2) + return round(self.api.data[self.entity_description.key], 2) except TypeError: - return self.api.data[self._condition] + return self.api.data[self.entity_description.key] @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index b0c4b09c2e7..dc699beb26b 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -58,6 +58,8 @@ async def async_setup_entry( class PiHoleSwitch(PiHoleEntity, SwitchEntity): """Representation of a Pi-hole switch.""" + _attr_icon = "mdi:pi-hole" + @property def name(self) -> str: """Return the name of the switch.""" @@ -68,11 +70,6 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity): """Return the unique id of the switch.""" return f"{self._server_unique_id}/Switch" - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return "mdi:pi-hole" - @property def is_on(self) -> bool: """Return if the service is on.""" diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 3a4d3582f9c..57f24180c03 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant @@ -30,7 +31,7 @@ async def async_setup_entry( return True -class PicnicSensor(CoordinatorEntity): +class PicnicSensor(SensorEntity, CoordinatorEntity): """The CoordinatorEntity subclass representing Picnic sensors.""" def __init__( @@ -49,7 +50,7 @@ class PicnicSensor(CoordinatorEntity): self._service_unique_id = config_entry.unique_id @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self.properties.get("unit") @@ -64,7 +65,7 @@ class PicnicSensor(CoordinatorEntity): return self._to_capitalized_name(self.sensor_type) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" data_set = ( self.coordinator.data.get(self.properties["data_type"], {}) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 02d56c890fe..5dbad2838bc 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -136,7 +136,7 @@ class CallRateDelayThrottle: def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) - self._queue = [] + self._queue: list = [] self._active = False self._lock = threading.Lock() self._next_ts = dt_util.utcnow() diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 97458acd5fc..bc8135c5932 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -63,12 +63,12 @@ class PilightSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 9af16a1cacd..e3e37d4291e 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -75,11 +75,11 @@ class PlaatoSensor(PlaatoEntity, SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._sensor_data.sensors.get(self._sensor_type) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor_data.get_unit_of_measurement(self._sensor_type) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 8ca72e8fb83..0969967e673 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -62,7 +62,7 @@ class PlexSensor(SensorEntity): self._attr_name = NAME_FORMAT.format(plex_server.friendly_name) self._attr_should_poll = False self._attr_unique_id = f"sensor-{plex_server.machine_identifier}" - self._attr_unit_of_measurement = "Watching" + self._attr_native_unit_of_measurement = "Watching" self._server = plex_server self.async_refresh_sensor = Debouncer( @@ -87,7 +87,7 @@ class PlexSensor(SensorEntity): async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - self._attr_state = len(self._server.sensor_attributes) + self._attr_native_value = len(self._server.sensor_attributes) self.async_write_ha_state() @property @@ -128,7 +128,7 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_name = f"{self.server_name} Library - {plex_library_section.title}" self._attr_should_poll = False self._attr_unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" - self._attr_unit_of_measurement = "Items" + self._attr_native_unit_of_measurement = "Items" async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -164,7 +164,7 @@ class PlexLibrarySectionSensor(SensorEntity): self.library_type, self.library_type ) - self._attr_state = self.library_section.totalViewSize( + self._attr_native_value = self.library_section.totalViewSize( libtype=primary_libtype, includeCollections=False ) for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []): diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 9168f070609..c0ecbe3e02c 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -10,8 +10,10 @@ }, "error": { "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", + "host_or_token": "Legal\u00e1bb egyet kell megadnia a Gazdag\u00e9p vagy a Token k\u00f6z\u00fcl", "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", - "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" + "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3", + "ssl_error": "SSL tan\u00fas\u00edtv\u00e1ny probl\u00e9ma" }, "flow_title": "{name} ({host})", "step": { @@ -22,7 +24,8 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "token": "Token (opcion\u00e1lis)", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "title": "K\u00e9zi Plex konfigur\u00e1ci\u00f3" }, "select_server": { "data": { @@ -32,9 +35,13 @@ "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" }, "user": { + "description": "Folytassa a [plex.tv] (https://plex.tv) oldalt a Plex szerver \u00f6sszekapcsol\u00e1s\u00e1hoz.", "title": "Plex Media Server" }, "user_advanced": { + "data": { + "setup_method": "Be\u00e1ll\u00edt\u00e1si m\u00f3dszer" + }, "title": "Plex Media Server" } } @@ -43,7 +50,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Hagyja figyelmen k\u00edv\u00fcl az \u00faj kezelt/megosztott felhaszn\u00e1l\u00f3kat", "ignore_plex_web_clients": "Plex Web kliensek figyelmen k\u00edv\u00fcl hagy\u00e1sa", + "monitored_users": "Megfigyelt felhaszn\u00e1l\u00f3k", "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" }, "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" diff --git a/homeassistant/components/plex/translations/zh-Hans.json b/homeassistant/components/plex/translations/zh-Hans.json index 9cc02584789..02f548f2286 100644 --- a/homeassistant/components/plex/translations/zh-Hans.json +++ b/homeassistant/components/plex/translations/zh-Hans.json @@ -1,11 +1,45 @@ { "config": { + "abort": { + "already_configured": "\u6b64 Plex \u670d\u52a1\u5668\u5df2\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "token_request_timeout": "\u83b7\u53d6\u4ee4\u724c\u8d85\u65f6", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "faulty_credentials": "\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u4ee4\u724c\u4fe1\u606f", + "host_or_token": "\u5fc5\u987b\u81f3\u5c11\u63d0\u4f9b\u4e00\u4e2a\u4e3b\u673a\u5730\u5740\u6216\u4ee4\u724c", + "not_found": "\u627e\u4e0d\u5230 Plex \u670d\u52a1\u5668", + "ssl_error": "SSL \u8bc1\u4e66\u9519\u8bef" + }, + "flow_title": "{name} ({host})", "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", + "token": "\u4ee4\u724c (\u53ef\u9009)", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + }, + "title": "\u624b\u52a8\u914d\u7f6e" + }, "select_server": { "data": { "server": "\u670d\u52a1\u5668" }, + "description": "\u6709\u591a\u4e2a\u53ef\u7528\u670d\u52a1\u5668\uff0c\u8bf7\u9009\u62e9\uff1a", "title": "\u9009\u62e9 Plex \u670d\u52a1\u5668" + }, + "user": { + "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" + }, + "user_advanced": { + "data": { + "setup_method": "\u8bbe\u7f6e\u65b9\u6cd5" + }, + "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" } } }, @@ -14,8 +48,10 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5171\u4eab\u4f7f\u7528\u8005", + "ignore_plex_web_clients": "\u5ffd\u7565 Plex Web \u5ba2\u6237\u7aef", "monitored_users": "\u53d7\u76d1\u89c6\u7684\u7528\u6237" - } + }, + "description": "Plex \u5a92\u4f53\u64ad\u653e\u5668\u9009\u9879" } } } diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 4152f9fdabd..854c2e6676c 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -272,12 +272,12 @@ class SmileSensor(SmileGateway, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of this entity.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 9f69c8579a4..f92d087b79d 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTAN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .utils import load_plum @@ -34,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Plum Lightpad Platform initialization.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 55ae4a524fc..f745bd562bd 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -50,7 +50,7 @@ class PocketCastsSensor(SensorEntity): return SENSOR_NAME @property - def state(self): + def native_value(self): """Return the sensor state.""" return self._state diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index dc34e1f9367..8d4ee69fca2 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -1,7 +1,14 @@ """Support for Minut Point sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -21,12 +28,48 @@ _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_SOUND = "sound_level" -SENSOR_TYPES = { - DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS), - DEVICE_CLASS_PRESSURE: (None, 0, PRESSURE_HPA), - DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE), - DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, SOUND_PRESSURE_WEIGHTED_DBA), -} + +@dataclass +class MinutPointRequiredKeysMixin: + """Mixin for required keys.""" + + precision: int + + +@dataclass +class MinutPointSensorEntityDescription( + SensorEntityDescription, MinutPointRequiredKeysMixin +): + """Describes MinutPoint sensor entity.""" + + +SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( + MinutPointSensorEntityDescription( + key="temperature", + precision=1, + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + ), + MinutPointSensorEntityDescription( + key="pressure", + precision=0, + device_class=DEVICE_CLASS_PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + ), + MinutPointSensorEntityDescription( + key="humidity", + precision=1, + device_class=DEVICE_CLASS_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + MinutPointSensorEntityDescription( + key="sound", + precision=1, + device_class=DEVICE_CLASS_SOUND, + icon="mdi:ear-hearing", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -36,10 +79,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Discover and add a discovered sensor.""" client = hass.data[POINT_DOMAIN][config_entry.entry_id] async_add_entities( - ( - MinutPointSensor(client, device_id, sensor_type) - for sensor_type in SENSOR_TYPES - ), + [ + MinutPointSensor(client, device_id, description) + for description in SENSOR_TYPES + ], True, ) @@ -51,10 +94,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MinutPointSensor(MinutPointEntity, SensorEntity): """The platform class required by Home Assistant.""" - def __init__(self, point_client, device_id, device_class): + entity_description: MinutPointSensorEntityDescription + + def __init__( + self, point_client, device_id, description: MinutPointSensorEntityDescription + ): """Initialize the sensor.""" - super().__init__(point_client, device_id, device_class) - self._device_prop = SENSOR_TYPES[device_class] + super().__init__(point_client, device_id, description.device_class) + self.entity_description = description async def _update_callback(self): """Update the value of the sensor.""" @@ -65,18 +112,8 @@ class MinutPointSensor(MinutPointEntity, SensorEntity): self.async_write_ha_state() @property - def icon(self): - """Return the icon representation.""" - return self._device_prop[0] - - @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.value is None: return None - return round(self.value, self._device_prop[1]) - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._device_prop[2] + return round(self.value, self.entity_description.precision) diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index dd03111e85e..e9aeaca20f5 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -89,7 +89,7 @@ class PoolSenseSensor(PoolSenseEntity, SensorEntity): return f"PoolSense {SENSORS[self.info_type]['name']}" @property - def state(self): + def native_value(self): """State of the sensor.""" return self.coordinator.data[self.info_type] @@ -104,7 +104,7 @@ class PoolSenseSensor(PoolSenseEntity, SensorEntity): return SENSORS[self.info_type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return SENSORS[self.info_type]["unit"] diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index c86333cb9f8..b2cd48df276 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -9,8 +9,6 @@ POWERWALL_API_CHANGED = "api_changed" UPDATE_INTERVAL = 30 ATTR_FREQUENCY = "frequency" -ATTR_ENERGY_EXPORTED = "energy_exported_(in_kW)" -ATTR_ENERGY_IMPORTED = "energy_imported_(in_kW)" ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" ATTR_INSTANT_TOTAL_CURRENT = "instant_total_current" ATTR_IS_ACTIVE = "is_active" diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index d536c776bf0..940dcad8647 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,17 +3,21 @@ import logging from tesla_powerwall import MeterType -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT, ) from .const import ( - ATTR_ENERGY_EXPORTED, - ATTR_ENERGY_IMPORTED, ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, ATTR_INSTANT_TOTAL_CURRENT, @@ -29,6 +33,11 @@ from .const import ( ) from .entity import PowerWallEntity +_METER_DIRECTION_EXPORT = "export" +_METER_DIRECTION_IMPORT = "import" +_METER_DIRECTIONS = [_METER_DIRECTION_EXPORT, _METER_DIRECTION_IMPORT] + + _LOGGER = logging.getLogger(__name__) @@ -55,6 +64,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): powerwalls_serial_numbers, ) ) + for meter_direction in _METER_DIRECTIONS: + entities.append( + PowerWallEnergyDirectionSensor( + meter, + coordinator, + site_info, + status, + device_type, + powerwalls_serial_numbers, + meter_direction, + ) + ) entities.append( PowerWallChargeSensor( @@ -69,7 +90,7 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall charge sensor.""" _attr_name = "Powerwall Charge" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = DEVICE_CLASS_BATTERY @property @@ -78,7 +99,7 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): return f"{self.base_unique_id}_charge" @property - def state(self): + def native_value(self): """Get the current value in percentage.""" return round(self.coordinator.data[POWERWALL_API_CHARGE]) @@ -87,7 +108,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_KILO_WATT + _attr_native_unit_of_measurement = POWER_KILO_WATT _attr_device_class = DEVICE_CLASS_POWER def __init__( @@ -110,7 +131,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Get the current value in kW.""" return ( self.coordinator.data[POWERWALL_API_METERS] @@ -124,9 +145,46 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) return { ATTR_FREQUENCY: round(meter.frequency, 1), - ATTR_ENERGY_EXPORTED: meter.get_energy_exported(), - ATTR_ENERGY_IMPORTED: meter.get_energy_imported(), ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), ATTR_IS_ACTIVE: meter.is_active(), } + + +class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): + """Representation of an Powerwall Direction Energy sensor.""" + + _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_device_class = DEVICE_CLASS_ENERGY + + def __init__( + self, + meter: MeterType, + coordinator, + site_info, + status, + device_type, + powerwalls_serial_numbers, + meter_direction, + ): + """Initialize the sensor.""" + super().__init__( + coordinator, site_info, status, device_type, powerwalls_serial_numbers + ) + self._meter = meter + self._meter_direction = meter_direction + self._attr_name = ( + f"Powerwall {self._meter.value.title()} {self._meter_direction.title()}" + ) + self._attr_unique_id = ( + f"{self.base_unique_id}_{self._meter.value}_{self._meter_direction}" + ) + + @property + def native_value(self): + """Get the current value in kWh.""" + meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) + if self._meter_direction == _METER_DIRECTION_EXPORT: + return meter.get_energy_exported() + return meter.get_energy_imported() diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 9f12342595a..d5bc30e7d11 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, "flow_title": "{ip_address}", "step": { @@ -15,7 +16,9 @@ "data": { "ip_address": "IP c\u00edm", "password": "Jelsz\u00f3" - } + }, + "description": "A jelsz\u00f3 \u00e1ltal\u00e1ban a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g sorozatsz\u00e1m\u00e1nak utols\u00f3 5 karaktere, \u00e9s megtal\u00e1lhat\u00f3 a Tesla alkalmaz\u00e1sban, vagy a jelsz\u00f3 utols\u00f3 5 karaktere a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g 2 ajtaj\u00e1ban.", + "title": "Csatlakoz\u00e1s a powerwallhoz" } } } diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json index 76af6fb124f..fea70ec88ac 100644 --- a/homeassistant/components/progettihwsw/translations/hu.json +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -33,7 +33,8 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "\u00c1ll\u00edtsa be" } } } diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json new file mode 100644 index 00000000000..fbccb2f6391 --- /dev/null +++ b/homeassistant/components/prosegur/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El sistema ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea", + "unknown": "Error desconocido" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Vuelva a autenticarse con su cuenta Prosegur.", + "password": "Clave", + "username": "Nombre de Usuario" + } + }, + "user": { + "data": { + "country": "Pa\u00eds", + "password": "Clave", + "username": "Nombre de Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/hu.json b/homeassistant/components/prosegur/translations/hu.json new file mode 100644 index 00000000000..143ae78d534 --- /dev/null +++ b/homeassistant/components/prosegur/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Hiteles\u00edtse \u00fajra Prosegur-fi\u00f3kkal.", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "country": "Orsz\u00e1g", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/no.json b/homeassistant/components/prosegur/translations/no.json index 5732bb920b2..73bacd26c14 100644 --- a/homeassistant/components/prosegur/translations/no.json +++ b/homeassistant/components/prosegur/translations/no.json @@ -1,14 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, "step": { "reauth_confirm": { "data": { + "description": "Autentiser p\u00e5 nytt med Prosegur-kontoen.", "password": "Passord", "username": "Brukernavn" } }, "user": { "data": { + "country": "Land", "password": "Passord", "username": "Brukernavn" } diff --git a/homeassistant/components/prosegur/translations/pt.json b/homeassistant/components/prosegur/translations/pt.json new file mode 100644 index 00000000000..d479d880d7f --- /dev/null +++ b/homeassistant/components/prosegur/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/zh-Hans.json b/homeassistant/components/prosegur/translations/zh-Hans.json new file mode 100644 index 00000000000..426e1f2919b --- /dev/null +++ b/homeassistant/components/prosegur/translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u6210\u529f", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u4f7f\u7528 Prosegur \u5e10\u6237\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, + "user": { + "data": { + "country": "\u56fd\u5bb6/\u5730\u533a", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 1b0d07c69a3..089e028afd1 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,4 +1,6 @@ """Support for Proxmox VE.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -18,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -84,7 +87,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the platform.""" hass.data.setdefault(DOMAIN, {}) @@ -132,7 +135,8 @@ async def async_setup(hass: HomeAssistant, config: dict): await hass.async_add_executor_job(build_client) - coordinators = hass.data[DOMAIN][COORDINATORS] = {} + coordinators: dict[str, dict[str, dict[int, DataUpdateCoordinator]]] = {} + hass.data[DOMAIN][COORDINATORS] = coordinators # Create a coordinator for each vm/container for host_config in config[DOMAIN]: diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 8fda507ace2..3c296b7d164 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,4 +1,6 @@ """Proxy camera platform that enables image processing of camera data.""" +from __future__ import annotations + import asyncio from datetime import timedelta import io @@ -219,13 +221,17 @@ class ProxyCamera(Camera): self._last_image = None self._mode = config.get(CONF_MODE) - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return camera image.""" return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" now = dt_util.utcnow() @@ -244,13 +250,13 @@ class ProxyCamera(Camera): job = _resize_image else: job = _crop_image - image = await self.hass.async_add_executor_job( + image_bytes: bytes = await self.hass.async_add_executor_job( job, image.content, self._image_opts ) if self._cache_images: - self._last_image = image - return image + self._last_image = image_bytes + return image_bytes async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index ff0ac45c139..8f4d1d04dcf 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,4 +1,6 @@ """Camera platform that receives images through HTTP POST.""" +from __future__ import annotations + import asyncio from collections import deque from datetime import timedelta @@ -155,7 +157,9 @@ class PushCamera(Camera): self.async_write_ha_state() - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response.""" if self.queue: if self._state == STATE_IDLE: diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 4f8ec6a1700..3585c198a51 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -79,7 +79,7 @@ class PushBulletNotificationSensor(SensorEntity): return f"Pushbullet {self._element}" @property - def state(self): + def native_value(self): """Return the current state of the sensor.""" return self._state diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 5744dbfff9a..8126e00d8e5 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,4 +1,6 @@ """Support for getting collected information from PVOutput.""" +from __future__ import annotations + from collections import namedtuple from datetime import timedelta import logging @@ -9,6 +11,7 @@ from homeassistant.components.rest.data import RestData from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -71,8 +74,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class PvoutputSensor(SensorEntity): """Representation of a PVOutput sensor.""" + _attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = ENERGY_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_WATT_HOUR def __init__(self, rest, name): """Initialize a PVOutput sensor.""" @@ -95,7 +99,7 @@ class PvoutputSensor(SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.pvcoutput is not None: return self.pvcoutput.energy_generation diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 3e98274c696..e628dfb9813 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity_registry import ( async_get, async_migrate_entries, ) +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_POWER, @@ -41,7 +42,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the electricity price sensor from configuration.yaml.""" for conf in config.get(DOMAIN, []): hass.async_create_task( diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 75881f93f0a..9cc5603e35b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -7,7 +7,7 @@ from typing import Any from aiopvpc import PVPCData -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback @@ -51,9 +51,10 @@ async def async_setup_entry( class ElecPriceSensor(RestoreEntity, SensorEntity): """Class to hold the prices of electricity as a sensor.""" - unit_of_measurement = UNIT - icon = ICON - should_poll = False + _attr_icon = ICON + _attr_native_unit_of_measurement = UNIT + _attr_should_poll = False + _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, name, unique_id, pvpc_data_handler): """Initialize the sensor object.""" @@ -106,7 +107,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" return self._pvpc_data.state diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 0b980bd58e0..1f706862ee1 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -6,10 +6,13 @@ "step": { "user": { "data": { + "name": "\u00c9rz\u00e9kel\u0151 neve", "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", - "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)" + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", + "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1nk\u00e9nt" }, - "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)." + "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index c439d5181be..f568b41776f 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -93,12 +93,12 @@ class PyLoadSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 89a7ab4ba04..922f5b71a3c 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -195,6 +195,7 @@ def execute(hass, filename, source, data=None): "sum": sum, "any": any, "all": all, + "enumerate": enumerate, } builtins = safe_builtins.copy() builtins.update(utility_builtins) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 251407099b1..4663b203248 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,11 +1,17 @@ """Support for monitoring the qBittorrent API.""" +from __future__ import annotations + import logging from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -25,11 +31,22 @@ SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" DEFAULT_NAME = "qBittorrent" -SENSOR_TYPES = { - SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=SENSOR_TYPE_CURRENT_STATUS, + name="Status", + ), + SensorEntityDescription( + key=SENSOR_TYPE_DOWNLOAD_SPEED, + name="Down Speed", + native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + ), + SensorEntityDescription( + key=SENSOR_TYPE_UPLOAD_SPEED, + name="Up Speed", + native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + ), +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -56,12 +73,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) - dev = [] - for sensor_type in SENSOR_TYPES: - sensor = QBittorrentSensor(sensor_type, client, name, LoginRequired) - dev.append(sensor) + entities = [ + QBittorrentSensor(description, client, name, LoginRequired) + for description in SENSOR_TYPES + ] - add_entities(dev, True) + add_entities(entities, True) def format_speed(speed): @@ -73,45 +90,29 @@ def format_speed(speed): class QBittorrentSensor(SensorEntity): """Representation of an qBittorrent sensor.""" - def __init__(self, sensor_type, qbittorrent_client, client_name, exception): + def __init__( + self, + description: SensorEntityDescription, + qbittorrent_client, + client_name, + exception, + ): """Initialize the qBittorrent sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.client = qbittorrent_client - self.type = sensor_type - self.client_name = client_name - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._available = False self._exception = exception - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + self._attr_name = f"{client_name} {description.name}" + self._attr_available = False def update(self): """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync_main_data() - self._available = True + self._attr_available = True except RequestException: _LOGGER.error("Connection lost") - self._available = False + self._attr_available = False return except self._exception: _LOGGER.error("Invalid authentication") @@ -123,17 +124,18 @@ class QBittorrentSensor(SensorEntity): download = data["server_state"]["dl_info_speed"] upload = data["server_state"]["up_info_speed"] - if self.type == SENSOR_TYPE_CURRENT_STATUS: + sensor_type = self.entity_description.key + if sensor_type == SENSOR_TYPE_CURRENT_STATUS: if upload > 0 and download > 0: - self._state = "up_down" + self._attr_native_value = "up_down" elif upload > 0 and download == 0: - self._state = "seeding" + self._attr_native_value = "seeding" elif upload == 0 and download > 0: - self._state = "downloading" + self._attr_native_value = "downloading" else: - self._state = STATE_IDLE + self._attr_native_value = STATE_IDLE - elif self.type == SENSOR_TYPE_DOWNLOAD_SPEED: - self._state = format_speed(download) - elif self.type == SENSOR_TYPE_UPLOAD_SPEED: - self._state = format_speed(upload) + elif sensor_type == SENSOR_TYPE_DOWNLOAD_SPEED: + self._attr_native_value = format_speed(download) + elif sensor_type == SENSOR_TYPE_UPLOAD_SPEED: + self._attr_native_value = format_speed(upload) 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/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index c175d89f60e..333ce46599a 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -243,7 +243,7 @@ class QNAPSensor(SensorEntity): return self.var_icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.var_units @@ -256,7 +256,7 @@ class QNAPCPUSensor(QNAPSensor): """A QNAP sensor that monitors CPU stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "cpu_temp": return self._api.data["system_stats"]["cpu"]["temp_c"] @@ -268,7 +268,7 @@ class QNAPMemorySensor(QNAPSensor): """A QNAP sensor that monitors memory stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" free = float(self._api.data["system_stats"]["memory"]["free"]) / 1024 if self.var_id == "memory_free": @@ -296,7 +296,7 @@ class QNAPNetworkSensor(QNAPSensor): """A QNAP sensor that monitors network stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "network_link_status": nic = self._api.data["system_stats"]["nics"][self.monitor_device] @@ -329,7 +329,7 @@ class QNAPSystemSensor(QNAPSensor): """A QNAP sensor that monitors overall system health.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "status": return self._api.data["system_health"] @@ -358,7 +358,7 @@ class QNAPDriveSensor(QNAPSensor): """A QNAP sensor that monitors HDD/SSD drive stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" data = self._api.data["smart_drive_health"][self.monitor_device] @@ -392,7 +392,7 @@ class QNAPVolumeSensor(QNAPSensor): """A QNAP sensor that monitors storage volume stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" data = self._api.data["volumes"][self.monitor_device] diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 2f4353063d1..cac288eaef0 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -1,4 +1,5 @@ """Support for QVR Pro streams.""" +from __future__ import annotations import logging @@ -88,7 +89,9 @@ class QVRProCamera(Camera): return attrs - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get image bytes from camera.""" try: return self._client.get_snapshot(self.guid) diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index f6d0ce7ec28..5de7bc0dccf 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -57,7 +57,7 @@ class QSSensor(QSEntity, SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the value of the sensor.""" return str(self._val) @@ -67,6 +67,6 @@ class QSSensor(QSEntity, SensorEntity): return f"qs{self.qsid}:{self.channel}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.unit diff --git a/homeassistant/components/rachio/translations/hu.json b/homeassistant/components/rachio/translations/hu.json index 570dd27b5d9..0c6112988d8 100644 --- a/homeassistant/components/rachio/translations/hu.json +++ b/homeassistant/components/rachio/translations/hu.json @@ -12,6 +12,17 @@ "user": { "data": { "api_key": "API kulcs" + }, + "description": "Sz\u00fcks\u00e9ge lesz az API-kulcsra a https://app.rach.io/ webhelyen. L\u00e9pjen a Be\u00e1ll\u00edt\u00e1sok elemre, majd kattintson az \u201eAPI KEY GET\u201d lek\u00e9r\u00e9s\u00e9re.", + "title": "Csatlakozzon a Rachio k\u00e9sz\u00fcl\u00e9khez" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "A fut\u00e1s id\u0151tartama percben a z\u00f3nakapcsol\u00f3 aktiv\u00e1l\u00e1sakor" } } } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 407f491f63a..02e898c8e0f 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -127,7 +127,7 @@ class RadarrSensor(SensorEntity): return "{} {}".format("Radarr", self._name) @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -137,7 +137,7 @@ class RadarrSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of the sensor.""" return self._unit diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2158bc5cf97..0f6ad41b4e3 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -45,6 +45,6 @@ class RainBirdSensor(SensorEntity): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self.name) if self.entity_description.key == SENSOR_TYPE_RAINSENSOR: - self._attr_state = self._controller.get_rain_sensor_state() + self._attr_native_value = self._controller.get_rain_sensor_state() elif self.entity_description.key == SENSOR_TYPE_RAINDELAY: - self._attr_state = self._controller.get_rain_delay() + self._attr_native_value = self._controller.get_rain_delay() diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index ee8f68734ad..c550e43285b 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -48,12 +48,12 @@ class RainCloudSensor(RainCloudEntity, SensorEntity): """A sensor implementation for raincloud device.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index fd28e5b0994..4b6268fd59a 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -4,5 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": ["eagle200_reader==0.2.4", "uEagle==0.0.2"], "codeowners": ["@gtdiehl", "@jcalbert"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [ + { + "macaddress": "D8D5B9*" + } + ] } diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 53e94d2070e..6e42d2a13a2 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging from eagle200_reader import EagleReader @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -22,7 +23,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, dt +from homeassistant.util import Throttle CONF_CLOUD_ID = "cloud_id" CONF_INSTALL_CODE = "install_code" @@ -41,7 +42,6 @@ class SensorType: unit_of_measurement: str device_class: str | None = None state_class: str | None = None - last_reset: datetime | None = None SENSORS = { @@ -54,15 +54,13 @@ SENSORS = { name="Eagle-200 Total Meter Energy Delivered", unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), "summation_received": SensorType( name="Eagle-200 Total Meter Energy Received", unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), "summation_total": SensorType( name="Eagle-200 Net Meter Energy (Delivered minus Received)", @@ -131,15 +129,14 @@ class EagleSensor(SensorEntity): self._type = sensor_type sensor_info = SENSORS[sensor_type] self._attr_name = sensor_info.name - self._attr_unit_of_measurement = sensor_info.unit_of_measurement + self._attr_native_unit_of_measurement = sensor_info.unit_of_measurement self._attr_device_class = sensor_info.device_class self._attr_state_class = sensor_info.state_class - self._attr_last_reset = sensor_info.last_reset def update(self): """Get the energy information from the Rainforest Eagle.""" self.eagle_data.update() - self._attr_state = self.eagle_data.get_state(self._type) + self._attr_native_value = self.eagle_data.get_state(self._type) class EagleData: diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 808c6a06bc2..2316b27acbf 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -134,7 +134,7 @@ class RainMachineSensor(RainMachineEntity, SensorEntity): self._attr_entity_registry_enabled_default = enabled_by_default self._attr_icon = icon self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit class ProvisionSettingsSensor(RainMachineSensor): @@ -144,7 +144,7 @@ class ProvisionSettingsSensor(RainMachineSensor): def update_from_latest_data(self) -> None: """Update the state.""" if self._entity_type == TYPE_FLOW_SENSOR_CLICK_M3: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorClicksPerCubicMeter" ) elif self._entity_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: @@ -154,15 +154,15 @@ class ProvisionSettingsSensor(RainMachineSensor): ) if clicks and clicks_per_m3: - self._attr_state = (clicks * 1000) / clicks_per_m3 + self._attr_native_value = (clicks * 1000) / clicks_per_m3 else: - self._attr_state = None + self._attr_native_value = None elif self._entity_type == TYPE_FLOW_SENSOR_START_INDEX: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorStartIndex" ) elif self._entity_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorWateringClicks" ) @@ -174,4 +174,4 @@ class UniversalRestrictionsSensor(RainMachineSensor): def update_from_latest_data(self) -> None: """Update the state.""" if self._entity_type == TYPE_FREEZE_TEMP: - self._attr_state = self.coordinator.data["freezeProtectTemp"] + self._attr_native_value = self.coordinator.data["freezeProtectTemp"] diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 6465b828be1..91a34639de1 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -58,7 +58,7 @@ class RandomSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -68,7 +68,7 @@ class RandomSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index beb7c182351..304eaafb85f 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -127,4 +127,4 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) - self._attr_state = as_utc(pickup_event.date).isoformat() + self._attr_native_value = as_utc(pickup_event.date).isoformat() diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e9c12e5f88a..e6c15729d24 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -174,6 +174,17 @@ async def async_migration_in_progress(hass: HomeAssistant) -> bool: return hass.data[DATA_INSTANCE].migration_in_progress +@bind_hass +def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: + """Check if an entity is being recorded. + + Async friendly. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].entity_filter(entity_id) + + def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 929115bdf25..fe75ba1cb50 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -20,8 +20,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.const import ( @@ -219,7 +218,6 @@ class StatisticData(TypedDict, total=False): mean: float min: float max: float - last_reset: datetime | None state: float sum: float @@ -243,7 +241,6 @@ class Statistics(Base): # type: ignore mean = Column(Float()) min = Column(Float()) max = Column(Float()) - last_reset = Column(DATETIME_TYPE) state = Column(Float()) sum = Column(Float()) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f3b0b27df39..6017f050419 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -11,13 +11,19 @@ from sqlalchemy import bindparam from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session -from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS +from homeassistant.const import ( + PRESSURE_PA, + TEMP_CELSIUS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util from homeassistant.util.unit_system import UnitSystem +import homeassistant.util.volume as volume_util from .const import DOMAIN from .models import ( @@ -37,7 +43,6 @@ QUERY_STATISTICS = [ Statistics.mean, Statistics.min, Statistics.max, - Statistics.last_reset, Statistics.state, Statistics.sum, ] @@ -64,6 +69,11 @@ UNIT_CONVERSIONS = { ) if x is not None else None, + VOLUME_CUBIC_METERS: lambda x, units: volume_util.convert( + x, VOLUME_CUBIC_METERS, _configured_unit(VOLUME_CUBIC_METERS, units) + ) + if x is not None + else None, } _LOGGER = logging.getLogger(__name__) @@ -214,6 +224,10 @@ def _configured_unit(unit: str, units: UnitSystem) -> str: return units.pressure_unit if unit == TEMP_CELSIUS: return units.temperature_unit + if unit == VOLUME_CUBIC_METERS: + if units.is_metric: + return VOLUME_CUBIC_METERS + return VOLUME_CUBIC_FEET return unit @@ -360,7 +374,6 @@ def _sorted_statistics_to_dict( "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), "max": convert(db_state.max, units), - "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), "state": convert(db_state.state, units), "sum": convert(db_state.sum, units), } diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 225eee6867f..e3af39b217a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -10,6 +10,7 @@ import os import time from typing import TYPE_CHECKING, Callable +from sqlalchemy import text from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session @@ -332,4 +333,5 @@ def perodic_db_cleanups(instance: Recorder): if instance.engine.dialect.name == "sqlite": # Execute sqlite to create a wal checkpoint and free up disk space _LOGGER.debug("WAL checkpoint") - instance.engine.execute("PRAGMA wal_checkpoint(TRUNCATE);") + with instance.engine.connect() as connection: + connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 7472ad42301..2e1ec5dc18a 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -99,7 +99,7 @@ class RedditSensor(SensorEntity): return f"reddit_{self._subreddit}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return len(self._subreddit_data) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 78b713c286c..99e2f90c879 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -105,7 +105,7 @@ class RejseplanenTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -131,7 +131,7 @@ class RejseplanenTransportSensor(SensorEntity): return attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 80433b2106e..d4c065e52ca 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) await renault_hub.async_initialise(config_entry) - hass.data[DOMAIN][config_entry.unique_id] = renault_hub + hass.data[DOMAIN][config_entry.entry_id] = renault_hub hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -40,6 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) if unload_ok: - hass.data[DOMAIN].pop(config_entry.unique_id) + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py new file mode 100644 index 00000000000..dd3ccb036e0 --- /dev/null +++ b/homeassistant/components/renault/binary_sensor.py @@ -0,0 +1,58 @@ +"""Support for Renault binary sensors.""" +from __future__ import annotations + +from renault_api.kamereon.enums import ChargeState, PlugState + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .renault_entities import RenaultBatteryDataEntity, RenaultDataEntity +from .renault_hub import RenaultHub + + +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.entry_id] + entities: list[RenaultDataEntity] = [] + for vehicle in proxy.vehicles.values(): + if "battery" in vehicle.coordinators: + entities.append(RenaultPluggedInSensor(vehicle, "Plugged In")) + entities.append(RenaultChargingSensor(vehicle, "Charging")) + async_add_entities(entities) + + +class RenaultPluggedInSensor(RenaultBatteryDataEntity, BinarySensorEntity): + """Plugged In binary sensor.""" + + _attr_device_class = DEVICE_CLASS_PLUG + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if (not self.data) or (self.data.plugStatus is None): + return None + return self.data.get_plug_status() == PlugState.PLUGGED + + +class RenaultChargingSensor(RenaultBatteryDataEntity, BinarySensorEntity): + """Charging binary sensor.""" + + _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if (not self.data) or (self.data.chargingStatus is None): + return None + return self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 51f6c10c6f1..0987d1829ed 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -7,6 +7,7 @@ CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" DEFAULT_SCAN_INTERVAL = 300 # 5 minutes PLATFORMS = [ + "binary_sensor", "sensor", ] diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 51e356934bb..b7a9b40e2c9 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -1,15 +1,25 @@ """Proxy to handle account communication with Renault servers.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon.models import KamereonVehiclesLink from renault_api.renault_account import RenaultAccount from renault_api.renault_client import RenaultClient from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_KAMEREON_ACCOUNT_ID, DEFAULT_SCAN_INTERVAL @@ -23,7 +33,6 @@ class RenaultHub: 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 @@ -48,18 +57,49 @@ class RenaultHub: self._account = await self._client.get_api_account(account_id) vehicles = await self._account.get_vehicles() + device_registry = dr.async_get(self._hass) 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 asyncio.gather( + *( + self.async_initialise_vehicle( + vehicle_link, + self._account, + scan_interval, + config_entry, + device_registry, ) - await vehicle.async_initialise() - self._vehicles[vehicle_link.vin] = vehicle + for vehicle_link in vehicles.vehicleLinks + ) + ) + + async def async_initialise_vehicle( + self, + vehicle_link: KamereonVehiclesLink, + renault_account: RenaultAccount, + scan_interval: timedelta, + config_entry: ConfigEntry, + device_registry: dr.DeviceRegistry, + ) -> None: + """Set up proxy.""" + assert vehicle_link.vin is not None + assert vehicle_link.vehicleDetails is not None + # Generate vehicle proxy + vehicle = RenaultVehicleProxy( + hass=self._hass, + vehicle=await renault_account.get_api_vehicle(vehicle_link.vin), + details=vehicle_link.vehicleDetails, + scan_interval=scan_interval, + ) + await vehicle.async_initialise() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers=vehicle.device_info[ATTR_IDENTIFIERS], + manufacturer=vehicle.device_info[ATTR_MANUFACTURER], + name=vehicle.device_info[ATTR_NAME], + model=vehicle.device_info[ATTR_MODEL], + sw_version=vehicle.device_info[ATTR_SW_VERSION], + ) + self._vehicles[vehicle_link.vin] = vehicle async def get_account_ids(self) -> list[str]: """Get Kamereon account ids.""" diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index d3f6b6e48be..8d4cfea53ee 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -115,7 +115,7 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.error( + LOGGER.warning( "Ignoring endpoint %s as it is not supported for this vehicle: %s", coordinator.name, coordinator.last_exception, @@ -123,7 +123,7 @@ class RenaultVehicleProxy: del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.error( + LOGGER.warning( "Ignoring endpoint %s as it is denied for this vehicle: %s", coordinator.name, coordinator.last_exception, diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 8403a04d001..7ef11fb2afc 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -1,14 +1,14 @@ """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_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, @@ -18,8 +18,6 @@ from homeassistant.const import ( ) 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, @@ -46,20 +44,20 @@ async def async_setup_entry( 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) + proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] + entities = get_entities(proxy) async_add_entities(entities) -async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: +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)) + entities.extend(get_vehicle_entities(vehicle)) return entities -async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: +def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: """Create Renault entities for single vehicle.""" entities: list[RenaultDataEntity] = [] if "cockpit" in vehicle.coordinators: @@ -78,6 +76,9 @@ async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultData entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power")) entities.append(RenaultPlugStateSensor(vehicle, "Plug State")) entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy")) + entities.append( + RenaultBatteryAvailableEnergySensor(vehicle, "Battery Available Energy") + ) entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature")) if "charge_mode" in vehicle.coordinators: entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode")) @@ -88,50 +89,46 @@ class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): """Battery autonomy sensor.""" _attr_icon = "mdi:ev-station" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.batteryAutonomy if self.data else None +class RenaultBatteryAvailableEnergySensor(RenaultBatteryDataEntity, SensorEntity): + """Battery available energy sensor.""" + + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + + @property + def native_value(self) -> float | None: + """Return the state of this entity.""" + return self.data.batteryAvailableEnergy if self.data else None + + class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): """Battery Level sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property - def state(self) -> int | None: + def native_value(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 + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.batteryTemperature if self.data else None @@ -142,7 +139,7 @@ class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_CHARGE_MODE @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of this entity.""" return self.data.chargeMode if self.data else None @@ -160,10 +157,10 @@ class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_CHARGE_STATE @property - def state(self) -> str | None: + def native_value(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 + return charging_status.name.lower() if charging_status is not None else None @property def icon(self) -> str: @@ -175,10 +172,10 @@ class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity) """Charging Remaining Time sensor.""" _attr_icon = "mdi:timer" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.chargingRemainingTime if self.data else None @@ -186,11 +183,11 @@ class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity) class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): """Charging Power sensor.""" - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = POWER_KILO_WATT + _attr_device_class = DEVICE_CLASS_POWER + _attr_native_unit_of_measurement = POWER_KILO_WATT @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of this entity.""" if not self.data or self.data.chargingInstantaneousPower is None: return None @@ -204,58 +201,52 @@ class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): """Fuel autonomy sensor.""" _attr_icon = "mdi:gas-station" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(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 - ) + if not self.data or self.data.fuelAutonomy is None: + return None + return round(self.data.fuelAutonomy) class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): """Fuel quantity sensor.""" _attr_icon = "mdi:fuel" - _attr_unit_of_measurement = VOLUME_LITERS + _attr_native_unit_of_measurement = VOLUME_LITERS @property - def state(self) -> int | None: + def native_value(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 - ) + if not self.data or self.data.fuelQuantity is None: + return None + return round(self.data.fuelQuantity) class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): """Mileage sensor.""" _attr_icon = "mdi:sign-direction" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(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 - ) + if not self.data or self.data.totalMileage is None: + return None + return round(self.data.totalMileage) class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): """HVAC Outside Temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of this entity.""" return self.data.externalTemperature if self.data else None @@ -266,10 +257,10 @@ class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_PLUG_STATE @property - def state(self) -> str | None: + def native_value(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 + return plug_status.name.lower() if plug_status is not None else None @property def icon(self) -> str: diff --git a/homeassistant/components/renault/translations/es-419.json b/homeassistant/components/renault/translations/es-419.json new file mode 100644 index 00000000000..6c895416ef8 --- /dev/null +++ b/homeassistant/components/renault/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Establecer las credenciales de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json new file mode 100644 index 00000000000..0eabcacccd3 --- /dev/null +++ b/homeassistant/components/renault/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon." + }, + "error": { + "invalid_credentials": "Autenticaci\u00f3n err\u00f3nea" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID de cuenta de Kamereon" + }, + "title": "Seleccione el id de la cuenta de Kamereon" + }, + "user": { + "data": { + "locale": "Configuraci\u00f3n regional", + "password": "Clave", + "username": "Correo-e" + }, + "title": "Establecer las credenciales de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json index d20e2d36a81..25cec1032e9 100644 --- a/homeassistant/components/renault/translations/he.json +++ b/homeassistant/components/renault/translations/he.json @@ -11,7 +11,8 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d3\u05d5\u05d0\"\u05dc" - } + }, + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e8\u05e0\u05d5" } } } diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json new file mode 100644 index 00000000000..eeace0b9b85 --- /dev/null +++ b/homeassistant/components/renault/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "kamereon_no_account": "Nem tal\u00e1lhat\u00f3 a Kamereon-fi\u00f3k." + }, + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon-fi\u00f3k azonos\u00edt\u00f3ja" + }, + "title": "V\u00e1lassza ki a Kamereon-fi\u00f3k azonos\u00edt\u00f3j\u00e1t" + }, + "user": { + "data": { + "locale": "Helysz\u00edn", + "password": "Jelsz\u00f3", + "username": "Email" + }, + "title": "\u00c1ll\u00edtsa be a Renault hiteles\u00edt\u0151 adatait" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json index f367c8c540d..4675f939fdd 100644 --- a/homeassistant/components/renault/translations/no.json +++ b/homeassistant/components/renault/translations/no.json @@ -1,11 +1,26 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "kamereon_no_account": "Kan ikke finne Kamereon -kontoen." + }, + "error": { + "invalid_credentials": "Ugyldig godkjenning" + }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon -konto -ID" + }, + "title": "Velg Kamereon -konto -ID" + }, "user": { "data": { + "locale": "Lokal", "password": "Passord", "username": "E-Post" - } + }, + "title": "Angi Renault-legitimasjon" } } } diff --git a/homeassistant/components/renault/translations/zh-Hans.json b/homeassistant/components/renault/translations/zh-Hans.json new file mode 100644 index 00000000000..ab8c60ed030 --- /dev/null +++ b/homeassistant/components/renault/translations/zh-Hans.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e", + "kamereon_no_account": "\u65e0\u6cd5\u627e\u5230 Kamereon \u5e10\u6237" + }, + "error": { + "invalid_credentials": "\u65e0\u6548\u8ba4\u8bc1" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon \u8d26\u53f7 ID" + }, + "title": "\u9009\u62e9 Kamereon \u8d26\u53f7 ID" + }, + "user": { + "data": { + "locale": "\u5730\u533a", + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u7bb1" + }, + "title": "\u8bbe\u7f6e Renault \u51ed\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 46818095647..04cff82bcf3 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -77,7 +77,7 @@ class RepetierSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self._sensor_type][1] @@ -92,7 +92,7 @@ class RepetierSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -134,7 +134,7 @@ class RepetierTempSensor(RepetierSensor): """Represent a Repetier temp sensor.""" @property - def state(self): + def native_value(self): """Return sensor state.""" if self._state is None: return None @@ -156,7 +156,7 @@ class RepetierJobSensor(RepetierSensor): """Represent a Repetier job sensor.""" @property - def state(self): + def native_value(self): """Return sensor state.""" if self._state is None: return None diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 8b9390bb1c9..42c342a2c84 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.entity_component import ( EntityComponent, ) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX @@ -43,7 +44,7 @@ PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"] COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the rest platforms.""" component = EntityComponent(_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 7727b5f09ab..f0355014986 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -115,12 +115,12 @@ class RestSensor(RestEntity, SensorEntity): self._json_attrs_path = json_attrs_path @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 497c9b8cee6..6b0c9efe157 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -152,12 +152,12 @@ class RflinkSensor(RflinkDevice, SensorEntity): self.handle_event_callback(self._initial_event) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return measurement unit.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return value.""" return self._state diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 44e1d537408..34b7c01600a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,6 @@ """Support for RFXtrx devices.""" import asyncio import binascii -from collections import OrderedDict import copy import functools import logging @@ -22,20 +21,7 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_PORT, - DEGREE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_STOP, - LENGTH_MILLIMETERS, - PERCENTAGE, - POWER_WATT, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, - UV_INDEX, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -66,38 +52,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 SIGNAL_EVENT = f"{DOMAIN}_event" -DATA_TYPES = OrderedDict( - [ - ("Temperature", TEMP_CELSIUS), - ("Temperature2", TEMP_CELSIUS), - ("Humidity", PERCENTAGE), - ("Barometer", PRESSURE_HPA), - ("Wind direction", DEGREE), - ("Rain rate", PRECIPITATION_MILLIMETERS_PER_HOUR), - ("Energy usage", POWER_WATT), - ("Total usage", ENERGY_KILO_WATT_HOUR), - ("Sound", None), - ("Sensor Status", None), - ("Counter value", "count"), - ("UV", UV_INDEX), - ("Humidity status", None), - ("Forecast", None), - ("Forecast numeric", None), - ("Rain total", LENGTH_MILLIMETERS), - ("Wind average speed", SPEED_METERS_PER_SECOND), - ("Wind gust", SPEED_METERS_PER_SECOND), - ("Chill", TEMP_CELSIUS), - ("Count", "count"), - ("Current Ch. 1", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 2", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 3", ELECTRIC_CURRENT_AMPERE), - ("Voltage", ELECTRIC_POTENTIAL_VOLT), - ("Current", ELECTRIC_CURRENT_AMPERE), - ("Battery numeric", PERCENTAGE), - ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT), - ] -) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 9e3d24cdb6a..f6751d760b2 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,4 +1,7 @@ """Support for RFXtrx binary sensors.""" +from __future__ import annotations + +from dataclasses import replace import logging import RFXtrx as rfxtrxmod @@ -7,6 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_SMOKE, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -51,13 +55,30 @@ SENSOR_STATUS_OFF = [ "Normal Tamper", ] -DEVICE_TYPE_DEVICE_CLASS = { - "X10 Security Motion Detector": DEVICE_CLASS_MOTION, - "KD101 Smoke Detector": DEVICE_CLASS_SMOKE, - "Visonic Powercode Motion Detector": DEVICE_CLASS_MOTION, - "Alecto SA30 Smoke Detector": DEVICE_CLASS_SMOKE, - "RM174RF Smoke Detector": DEVICE_CLASS_SMOKE, -} +SENSOR_TYPES = ( + BinarySensorEntityDescription( + key="X10 Security Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="KD101 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="Visonic Powercode Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="Alecto SA30 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="RM174RF Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} def supported(event): @@ -85,6 +106,14 @@ async def async_setup_entry( discovery_info = config_entry.data + def get_sensor_description(type_string: str, device_class: str | None = None): + description = SENSOR_TYPES_DICT.get(type_string) + if description is None: + description = BinarySensorEntityDescription(key=type_string) + if device_class: + description = replace(description, device_class=device_class) + return description + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) if event is None: @@ -107,9 +136,8 @@ async def async_setup_entry( device = RfxtrxBinarySensor( event.device, device_id, - entity_info.get( - CONF_DEVICE_CLASS, - DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + get_sensor_description( + event.device.type_string, entity_info.get(CONF_DEVICE_CLASS) ), entity_info.get(CONF_OFF_DELAY), entity_info.get(CONF_DATA_BITS), @@ -137,11 +165,12 @@ async def async_setup_entry( event.device.subtype, "".join(f"{x:02x}" for x in event.data), ) + sensor = RfxtrxBinarySensor( event.device, device_id, event=event, - device_class=DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + entity_description=get_sensor_description(event.device.type_string), ) async_add_entities([sensor]) @@ -156,7 +185,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self, device, device_id, - device_class=None, + entity_description, off_delay=None, data_bits=None, cmd_on=None, @@ -165,7 +194,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): ): """Initialize the RFXtrx sensor.""" super().__init__(device, device_id, event=event) - self._device_class = device_class + self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay self._state = None @@ -190,11 +219,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return the sensor class.""" - return self._device_class - @property def is_on(self): """Return true if the sensor state is True.""" diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 72cd9f6bbf6..7ce986d7082 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,5 +1,9 @@ """Support for RFXtrx sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Callable from RFXtrx import ControlEvent, SensorEvent @@ -8,21 +12,36 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( CONF_DEVICES, + DEGREE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + LENGTH_MILLIMETERS, + PERCENTAGE, + POWER_WATT, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + UV_INDEX, ) from homeassistant.core import callback from . import ( CONF_DATA_BITS, - DATA_TYPES, RfxtrxEntity, connect_auto_add, get_device_id, @@ -47,25 +66,158 @@ def _rssi_convert(value): return f"{value*8-120}" -DEVICE_CLASSES = { - "Barometer": DEVICE_CLASS_PRESSURE, - "Battery numeric": DEVICE_CLASS_BATTERY, - "Current Ch. 1": DEVICE_CLASS_CURRENT, - "Current Ch. 2": DEVICE_CLASS_CURRENT, - "Current Ch. 3": DEVICE_CLASS_CURRENT, - "Energy usage": DEVICE_CLASS_POWER, - "Humidity": DEVICE_CLASS_HUMIDITY, - "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, - "Temperature": DEVICE_CLASS_TEMPERATURE, - "Total usage": DEVICE_CLASS_ENERGY, - "Voltage": DEVICE_CLASS_VOLTAGE, -} +@dataclass +class RfxtrxSensorEntityDescription(SensorEntityDescription): + """Description of sensor entities.""" + + convert: Callable = lambda x: x -CONVERT_FUNCTIONS = { - "Battery numeric": _battery_convert, - "Rssi numeric": _rssi_convert, -} +SENSOR_TYPES = ( + RfxtrxSensorEntityDescription( + key="Barameter", + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ), + RfxtrxSensorEntityDescription( + key="Battery numeric", + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + convert=_battery_convert, + ), + RfxtrxSensorEntityDescription( + key="Current", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 1", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 2", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 3", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Energy usage", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + RfxtrxSensorEntityDescription( + key="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ), + RfxtrxSensorEntityDescription( + key="Rssi numeric", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + convert=_rssi_convert, + ), + RfxtrxSensorEntityDescription( + key="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Temperature2", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Total usage", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + RfxtrxSensorEntityDescription( + key="Wind direction", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=DEGREE, + ), + RfxtrxSensorEntityDescription( + key="Rain rate", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Sound", + ), + RfxtrxSensorEntityDescription( + key="Sensor Status", + ), + RfxtrxSensorEntityDescription( + key="Count", + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Counter value", + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Chill", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Wind average speed", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Wind gust", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Rain total", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + RfxtrxSensorEntityDescription( + key="Forecast", + ), + RfxtrxSensorEntityDescription( + key="Forecast numeric", + ), + RfxtrxSensorEntityDescription( + key="Humidity status", + ), + RfxtrxSensorEntityDescription( + key="UV", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=UV_INDEX, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} async def async_setup_entry( @@ -92,13 +244,13 @@ async def async_setup_entry( device_id = get_device_id( event.device, data_bits=entity_info.get(CONF_DATA_BITS) ) - for data_type in set(event.values) & set(DATA_TYPES): + for data_type in set(event.values) & set(SENSOR_TYPES_DICT): data_id = (*device_id, data_type) if data_id in data_ids: continue data_ids.add(data_id) - entity = RfxtrxSensor(event.device, device_id, data_type) + entity = RfxtrxSensor(event.device, device_id, SENSOR_TYPES_DICT[data_type]) entities.append(entity) async_add_entities(entities) @@ -109,7 +261,7 @@ async def async_setup_entry( if not supported(event): return - for data_type in set(event.values) & set(DATA_TYPES): + for data_type in set(event.values) & set(SENSOR_TYPES_DICT): data_id = (*device_id, data_type) if data_id in data_ids: continue @@ -123,7 +275,9 @@ async def async_setup_entry( "".join(f"{x:02x}" for x in event.data), ) - entity = RfxtrxSensor(event.device, device_id, data_type, event=event) + entity = RfxtrxSensor( + event.device, device_id, SENSOR_TYPES_DICT[data_type], event=event + ) async_add_entities([entity]) # Subscribe to main RFXtrx events @@ -133,16 +287,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" - def __init__(self, device, device_id, data_type, event=None): + entity_description: RfxtrxSensorEntityDescription + + def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) - self.data_type = data_type - self._unit_of_measurement = DATA_TYPES.get(data_type) - self._name = f"{device.type_string} {device.id_string} {data_type}" - self._unique_id = "_".join(x for x in (*self._device_id, data_type)) - - self._device_class = DEVICE_CLASSES.get(data_type) - self._convert_fun = CONVERT_FUNCTIONS.get(data_type, lambda x: x) + self.entity_description = entity_description + self._name = f"{device.type_string} {device.id_string} {entity_description.key}" + self._unique_id = "_".join( + x for x in (*self._device_id, entity_description.key) + ) async def async_added_to_hass(self): """Restore device state.""" @@ -156,17 +310,12 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): self._apply_event(get_rfx_object(event)) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if not self._event: return None - value = self._event.values.get(self.data_type) - return self._convert_fun(value) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement + value = self._event.values.get(self.entity_description.key) + return self.entity_description.convert(value) @property def should_poll(self): @@ -178,18 +327,13 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return a device class for sensor.""" - return self._device_class - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" if device_id != self._device_id: return - if self.data_type not in event.values: + if self.entity_description.key not in event.values: return _LOGGER.debug( diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index d8a27a3173b..5b953c1260e 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -69,7 +69,8 @@ "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", - "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma" + "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma", + "venetian_blind_mode": "Velencei red\u0151ny \u00fczemm\u00f3d" }, "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" } diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 580fc71e141..509877ae5ff 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,13 +1,14 @@ """This component provides support to the Ring Door Bell camera.""" -import asyncio +from __future__ import annotations + from datetime import timedelta from itertools import chain import logging from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import requests +from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION @@ -42,12 +43,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, config_entry_id, ffmpeg, device): + def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) self._name = self._device.name - self._ffmpeg = ffmpeg + self._ffmpeg_manager = ffmpeg_manager self._last_event = None self._last_video_id = None self._video_url = None @@ -101,27 +102,23 @@ class RingCam(RingEntityMixin, Camera): "last_video_id": self._last_video_id, } - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary) - if self._video_url is None: return - image = await asyncio.shield( - ffmpeg.get_image( - self._video_url, - output_format=IMAGE_JPEG, - ) + return await ffmpeg.async_get_image( + self.hass, self._video_url, width=width, height=height ) - return image async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: return - stream = CameraMjpeg(self._ffmpeg.binary) + stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) try: @@ -130,7 +127,7 @@ class RingCam(RingEntityMixin, Camera): self.hass, request, stream_reader, - self._ffmpeg.ffmpeg_stream_content_type, + self._ffmpeg_manager.ffmpeg_stream_content_type, ) finally: await stream.close() diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 97fb8ec9d21..192ba03c010 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -56,7 +56,7 @@ class RingSensor(RingEntityMixin, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._sensor_type == "volume": return self._device.volume @@ -84,7 +84,7 @@ class RingSensor(RingEntityMixin, SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] @@ -120,7 +120,7 @@ class HealthDataRingSensor(RingSensor): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._sensor_type == "wifi_signal_category": return self._device.wifi_signal_category @@ -172,7 +172,7 @@ class HistoryRingSensor(RingSensor): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._latest_event is None: return None diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index f36e2c58ec8..2746f5789cd 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -46,12 +46,12 @@ class RippleSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index b39655949b2..0068e8c0f04 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -87,7 +87,7 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Value of sensor.""" if self._event is None: return None diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index a5ebcab51b5..77d842353fc 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -44,7 +44,7 @@ "B": "Gruppe B", "C": "Gruppe C", "D": "Gruppe D", - "arm": "Aktiv, abwesend", + "arm": "Aktiv (abwesend)", "partial_arm": "Teilweise aktiv (STAY)" }, "description": "W\u00e4hle aus, welchen Zustand dein Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index ee2a517a3f7..8a9ed5d94a3 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hublot = device.hublot coordinator = RitualsDataUpdateCoordinator(hass, device) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 7c957722384..c4b330d1ccf 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -59,7 +59,7 @@ class DiffuserPerfumeSensor(DiffuserEntity): return "mdi:tag-remove" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the perfume sensor.""" return self._diffuser.perfume @@ -81,7 +81,7 @@ class DiffuserFillSensor(DiffuserEntity): return "mdi:beaker-question" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the fill sensor.""" return self._diffuser.fill @@ -90,7 +90,7 @@ class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -99,7 +99,7 @@ class DiffuserBatterySensor(DiffuserEntity): super().__init__(diffuser, coordinator, BATTERY_SUFFIX) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the battery sensor.""" return self._diffuser.battery_percentage @@ -108,7 +108,7 @@ class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -117,6 +117,6 @@ class DiffuserWifiSensor(DiffuserEntity): super().__init__(diffuser, coordinator, WIFI_SUFFIX) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the wifi sensor.""" return self._diffuser.wifi_percentage diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 9e4e7f3d588..bf2eab2d7b7 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -145,7 +145,7 @@ class RMVDepartureSensor(SensorEntity): return self._state is not None @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state @@ -171,7 +171,7 @@ class RMVDepartureSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index 5485d9e00ce..b7aa12bfb4d 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -19,13 +19,18 @@ "title": "Roku" }, "ssdp_confirm": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", "title": "Roku" }, "user": { "data": { "host": "Hoszt" - } + }, + "description": "Adja meg Roku adatait." } } } diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 4a99d9f71af..bc20b4397e2 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -36,7 +36,7 @@ class RoombaBattery(IRobotEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return PERCENTAGE @@ -50,6 +50,6 @@ class RoombaBattery(IRobotEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._battery_level diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 2f8d902f4fe..0d76ce920b2 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -40,10 +40,12 @@ "user": { "data": { "blid": "BLID", + "continuous": "Folyamatos", "delay": "K\u00e9sleltet\u00e9s", "host": "Hoszt", "password": "Jelsz\u00f3" }, + "description": "V\u00e1lasszon Roomba-t vagy Braava-t.", "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" } } diff --git a/homeassistant/components/roomba/translations/zh-Hans.json b/homeassistant/components/roomba/translations/zh-Hans.json new file mode 100644 index 00000000000..7674a49c492 --- /dev/null +++ b/homeassistant/components/roomba/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "not_irobot_device": "\u5df2\u53d1\u73b0\u7684\u8bbe\u5907\u5e76\u4e0d\u662f iRobot \u8bbe\u5907" + }, + "step": { + "manual": { + "description": "\u672a\u5728\u60a8\u7684\u7f51\u7edc\u4e0a\u53d1\u73b0 Roomba \u6216 Braava\u3002 BLID \u662f\u8bbe\u5907\u4e3b\u673a\u540d\u4e2d \u201ciRobot-\u201d \u6216 \u201cRoomba-\u201d \u4e4b\u540e\u7684\u90e8\u5206\u3002\u8bf7\u6309\u7167\u6587\u6863\u4e2d\u6982\u8ff0\u7684\u6b65\u9aa4\u64cd\u4f5c\uff1a {auth_help_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 123027a8216..56a8ade165c 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -9,12 +9,14 @@ }, "step": { "link": { + "description": "Enged\u00e9lyeznie kell az HomeAssistantot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a HomeAssistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", "title": "Enged\u00e9lyezze a HomeAssistant alkalmaz\u00e1st Roon-ban" }, "user": { "data": { "host": "Hoszt" - } + }, + "description": "Nem tal\u00e1lta a Roon szervert, adja meg a gazdag\u00e9p nev\u00e9t vagy IP-c\u00edm\u00e9t." } } } diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 35d8c0ae2c0..54e2c315a4e 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -116,7 +116,7 @@ class RovaSensor(SensorEntity): self.data_service.update() pickup_date = self.data_service.data.get(self.entity_description.key) if pickup_date is not None: - self._attr_state = pickup_date.isoformat() + self._attr_native_value = pickup_date.isoformat() class RovaData: diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 070e861b3c9..980586d4def 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,4 +1,6 @@ """Camera platform that has a Raspberry Pi camera.""" +from __future__ import annotations + import logging import os import shutil @@ -122,7 +124,9 @@ class RaspberryCamera(Camera): ): pass - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return raspistill image response.""" with open(self._config[CONF_FILE_PATH], "rb") as file: return file.read() diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index c750c7aa83c..5379cb2ce2e 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -94,7 +94,7 @@ class RTorrentSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -104,7 +104,7 @@ class RTorrentSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 8574e82aa47..a420ca53814 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -31,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "sabnzbd" DATA_SABNZBD = "sabznbd" -_CONFIGURING = {} +_CONFIGURING: dict[str, str] = {} ATTR_SPEED = "speed" BASE_URL_FORMAT = "{}://{}:{}/" diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index c0930f2c114..ffe57e608bf 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -45,7 +45,7 @@ class SabnzbdSensor(SensorEntity): return f"{self._client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -55,7 +55,7 @@ class SabnzbdSensor(SensorEntity): return False @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/safe_mode/__init__.py b/homeassistant/components/safe_mode/__init__.py index 94bd95aabe0..162dd204c54 100644 --- a/homeassistant/components/safe_mode/__init__.py +++ b/homeassistant/components/safe_mode/__init__.py @@ -1,11 +1,12 @@ """The Safe Mode integration.""" from homeassistant.components import persistent_notification from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType DOMAIN = "safe_mode" -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Safe Mode component.""" persistent_notification.async_create( hass, diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 1b46632051e..8e59899de27 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -34,7 +35,6 @@ from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later -from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -177,10 +177,10 @@ class SAJsensor(SensorEntity): self._serialnumber = serialnumber self._state = self._sensor.value - if pysaj_sensor.name in ("current_power", "total_yield", "temperature"): + if pysaj_sensor.name in ("current_power", "temperature"): self._attr_state_class = STATE_CLASS_MEASUREMENT if pysaj_sensor.name == "total_yield": - self._attr_last_reset = dt_util.utc_from_timestamp(0) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @property def name(self): @@ -191,12 +191,12 @@ class SAJsensor(SensorEntity): return f"saj_{self._sensor.name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return SAJ_UNIT_MAPPINGS[self._sensor.unit] diff --git a/homeassistant/components/samsungtv/translations/zh-Hans.json b/homeassistant/components/samsungtv/translations/zh-Hans.json new file mode 100644 index 00000000000..da6a5c3c9ba --- /dev/null +++ b/homeassistant/components/samsungtv/translations/zh-Hans.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u5df2\u5728\u8fdb\u884c\u4e2d", + "auth_missing": "Home Assistant \u672a\u88ab\u5141\u8bb8\u8fde\u63a5\u6b64\u4e09\u661f\u7535\u89c6\u3002\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u89c6\u8bbe\u7f6e\u3002", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "id_missing": "\u6b64\u4e09\u661f\u8bbe\u5907\u6ca1\u6709\u5e8f\u5217\u53f7\u3002", + "not_supported": "\u6b64\u4e09\u661f\u8bbe\u5907\u76ee\u524d\u6682\u4e0d\u652f\u6301\u3002", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "auth_missing": "Home Assistant \u672a\u88ab\u5141\u8bb8\u8fde\u63a5\u6b64\u4e09\u661f\u7535\u89c6\u3002\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u89c6\u8bbe\u7f6e\u3002" + }, + "flow_title": "{device}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u914d\u7f6e {device} ?\n\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002", + "title": "\u4e09\u661f\u7535\u89c6" + }, + "reauth_confirm": { + "description": "\u63d0\u4ea4\u4fe1\u606f\u540e\uff0c\u8bf7\u5728 30 \u79d2\u5185\u5728 {device} \u540c\u610f\u83b7\u53d6\u76f8\u5173\u6388\u6743\u3002" + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u7528\u6237\u540d" + }, + "description": "\u8f93\u5165\u60a8\u7684\u4e09\u661f\u7535\u89c6\u4fe1\u606f\u3002\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 921ab29f714..1f5b543f9ee 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -108,12 +108,12 @@ class ScrapeSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 223ca9262ee..2ec087d1e61 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -31,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch", "sensor", "binary_sensor", "climate"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Screenlogic component.""" domain_data = hass.data[DOMAIN] = {} domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass) diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 1ad18298655..c8e4f84caf0 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -114,7 +114,7 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): return f"{self.gateway_name} {self.sensor['name']}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.sensor.get("unit") @@ -125,7 +125,7 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property - def state(self): + def native_value(self): """State of the sensor.""" value = self.sensor["value"] return (value - 1) if "supply" in self._data_key else value @@ -160,7 +160,7 @@ class ScreenLogicChemistrySensor(ScreenLogicSensor): self._key = key @property - def state(self): + def native_value(self): """State of the sensor.""" value = self.sensor["value"] if "dosing_state" in self._key: diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index fc13b8ca098..5472ac421c3 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -11,12 +11,13 @@ from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity import entity_sources as get_entity_sources +from homeassistant.helpers.typing import ConfigType DOMAIN = "search" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Search component.""" websocket_api.async_register_command(hass, websocket_search_related) return True diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 165920dd8e5..80fb71f594b 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -126,7 +126,7 @@ class Season(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the current season.""" return self.season diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 0bde2f7a7a7..16cecd1cd97 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.9.0"], + "requirements": ["sense_energy==0.9.2"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 5a352969c3b..6be24a73a21 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,9 @@ """Support for monitoring a Sense energy sensor.""" -import datetime - -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_ENERGY, @@ -12,7 +14,6 @@ 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, @@ -125,7 +126,7 @@ class SenseActiveSensor(SensorEntity): """Implementation of a Sense energy sensor.""" _attr_icon = ICON - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_should_poll = False _attr_available = False @@ -168,9 +169,9 @@ class SenseActiveSensor(SensorEntity): if self._is_production else self._data.active_power ) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return - self._attr_state = new_state + self._attr_native_value = new_state self._attr_available = True self.async_write_ha_state() @@ -178,7 +179,7 @@ class SenseActiveSensor(SensorEntity): class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" - _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT + _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False @@ -212,10 +213,10 @@ class SenseVoltageSensor(SensorEntity): def _async_update_from_data(self): """Update the sensor from the data. Must not do I/O.""" new_state = round(self._data.active_voltage[self._voltage_index], 1) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return self._attr_available = True - self._attr_state = new_state + self._attr_native_value = new_state self.async_write_ha_state() @@ -223,8 +224,8 @@ 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_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False @@ -249,7 +250,7 @@ class SenseTrendsSensor(SensorEntity): self._had_any_update = False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._data.get_trend(self._sensor_type, self._is_production), 1) @@ -258,13 +259,6 @@ 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.""" @@ -288,7 +282,7 @@ class SenseEnergyDevice(SensorEntity): _attr_available = False _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_device_class = DEVICE_CLASS_POWER _attr_should_poll = False @@ -320,8 +314,8 @@ class SenseEnergyDevice(SensorEntity): new_state = 0 else: new_state = int(device_data["w"]) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return - self._attr_state = new_state + self._attr_native_value = new_state self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json index 4ecaf2ba0d0..acd67b9e6f9 100644 --- a/homeassistant/components/sense/translations/hu.json +++ b/homeassistant/components/sense/translations/hu.json @@ -13,7 +13,8 @@ "data": { "email": "E-mail", "password": "Jelsz\u00f3" - } + }, + "title": "Csatlakoztassa a Sense Energy Monitort" } } } diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 379301b0fa7..9274f133441 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -86,12 +86,12 @@ class SenseHatSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 51404ed7c40..84d0a404304 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -11,21 +12,33 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -34,11 +47,11 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, StateType _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" +ATTR_LAST_RESET: Final = "last_reset" # Deprecated, to be removed in 2021.11 ATTR_STATE_CLASS: Final = "state_class" DOMAIN: Final = "sensor" @@ -47,6 +60,7 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" SCAN_INTERVAL: Final = timedelta(seconds=30) DEVICE_CLASSES: Final[list[str]] = [ + DEVICE_CLASS_AQI, # Air Quality Index DEVICE_CLASS_BATTERY, # % of battery that is left DEVICE_CLASS_CO, # ppm (parts per million) Carbon Monoxide gas concentration DEVICE_CLASS_CO2, # ppm (parts per million) Carbon Dioxide gas concentration @@ -55,13 +69,22 @@ DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_MONETARY, # Amount of money (currency) + DEVICE_CLASS_OZONE, # Amount of O3 (µg/m³) + DEVICE_CLASS_NITROGEN_DIOXIDE, # Amount of NO2 (µg/m³) + DEVICE_CLASS_NITROUS_OXIDE, # Amount of NO (µg/m³) + DEVICE_CLASS_NITROGEN_MONOXIDE, # Amount of N2O (µg/m³) + DEVICE_CLASS_PM1, # Particulate matter <= 0.1 μm (µg/m³) + DEVICE_CLASS_PM10, # Particulate matter <= 10 μm (µg/m³) + DEVICE_CLASS_PM25, # Particulate matter <= 2.5 μm (µg/m³) DEVICE_CLASS_SIGNAL_STRENGTH, # signal strength (dB/dBm) + DEVICE_CLASS_SULPHUR_DIOXIDE, # Amount of SO2 (µg/m³) DEVICE_CLASS_TEMPERATURE, # temperature (C/F) DEVICE_CLASS_TIMESTAMP, # timestamp (ISO8601) DEVICE_CLASS_PRESSURE, # pressure (hPa/mbar) DEVICE_CLASS_POWER, # power (W/kW) DEVICE_CLASS_POWER_FACTOR, # power factor (%) DEVICE_CLASS_VOLTAGE, # voltage (V) + DEVICE_CLASS_GAS, # gas (m³ or ft³) ] DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) @@ -69,8 +92,13 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) CONF_STATE_CLASS: Final = "state_class" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" +# The state represents a monotonically increasing total, e.g. an amount of consumed gas +STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" -STATE_CLASSES: Final[list[str]] = [STATE_CLASS_MEASUREMENT] +STATE_CLASSES: Final[list[str]] = [ + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +] STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(STATE_CLASSES)) @@ -102,15 +130,20 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" state_class: str | None = None - last_reset: datetime | None = None + last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 + native_unit_of_measurement: str | None = None class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription + _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 + _attr_native_unit_of_measurement: str | None + _attr_native_value: StateType = None _attr_state_class: str | None - _attr_last_reset: datetime | None + _last_reset_reported = False + _temperature_conversion_reported = False @property def state_class(self) -> str | None: @@ -122,7 +155,7 @@ class SensorEntity(Entity): return None @property - def last_reset(self) -> datetime | None: + def last_reset(self) -> datetime | None: # Deprecated, to be removed in 2021.11 """Return the time when the sensor was last reset, if any.""" if hasattr(self, "_attr_last_reset"): return self._attr_last_reset @@ -143,6 +176,115 @@ class SensorEntity(Entity): def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if last_reset := self.last_reset: + if ( + last_reset is not None + and self.state_class == STATE_CLASS_MEASUREMENT + and not self._last_reset_reported + ): + self._last_reset_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) with state_class %s has set last_reset. Setting " + "last_reset is deprecated and will be unsupported from Home " + "Assistant Core 2021.11. Please update your configuration if " + "state_class is manually configured, otherwise %s", + self.entity_id, + type(self), + self.state_class, + report_issue, + ) + return {ATTR_LAST_RESET: last_reset.isoformat()} return None + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self._attr_native_value + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if hasattr(self, "_attr_native_unit_of_measurement"): + return self._attr_native_unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the entity, after unit conversion.""" + if ( + hasattr(self, "_attr_unit_of_measurement") + and self._attr_unit_of_measurement is not None + ): + return self._attr_unit_of_measurement + if ( + hasattr(self, "entity_description") + and self.entity_description.unit_of_measurement is not None + ): + return self.entity_description.unit_of_measurement + + native_unit_of_measurement = self.native_unit_of_measurement + + if native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + return self.hass.config.units.temperature_unit + + return native_unit_of_measurement + + @property + def state(self) -> Any: + """Return the state of the sensor and perform unit conversions, if needed.""" + # Test if _attr_state has been set in this instance + if "_attr_state" in self.__dict__: + return self._attr_state + + unit_of_measurement = self.native_unit_of_measurement + value = self.native_value + + units = self.hass.config.units + if ( + value is not None + and unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) + and unit_of_measurement != units.temperature_unit + ): + if ( + self.device_class != DEVICE_CLASS_TEMPERATURE + and not self._temperature_conversion_reported + ): + self._temperature_conversion_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) with device_class %s reports a temperature in " + "%s which will be converted to %s. Temperature conversion for " + "entities without correct device_class is deprecated and will" + " be removed from Home Assistant Core 2022.3. Please update " + "your configuration if device_class is manually configured, " + "otherwise %s", + self.entity_id, + type(self), + self.device_class, + unit_of_measurement, + units.temperature_unit, + report_issue, + ) + value_s = str(value) + prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 + # Suppress ValueError (Could not convert sensor_value to float) + with suppress(ValueError): + temp = units.temperature(float(value), unit_of_measurement) + value = str(round(temp) if prec == 0 else round(temp, prec)) + + return value + + def __repr__(self) -> str: + """Return the representation. + + Entity.__repr__ includes the state in the generated string, this fails if we're + called before self.hass is set. + """ + if not self.hass: + return f"" + + return super().__repr__() diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index a77ed2d2cd7..dee20405e07 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -16,12 +16,21 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ) @@ -46,11 +55,20 @@ CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CURRENT = "is_current" CONF_IS_ENERGY = "is_energy" CONF_IS_HUMIDITY = "is_humidity" +CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_NITROGEN_DIOXIDE = "is_nitrogen_dioxide" +CONF_IS_NITROGEN_MONOXIDE = "is_nitrogen_monoxide" +CONF_IS_NITROUS_OXIDE = "is_nitrous_oxide" +CONF_IS_OZONE = "is_ozone" +CONF_IS_PM1 = "is_pm1" +CONF_IS_PM10 = "is_pm10" +CONF_IS_PM25 = "is_pm25" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_PRESSURE = "is_pressure" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" +CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" CONF_IS_TEMPERATURE = "is_temperature" CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" @@ -61,12 +79,21 @@ ENTITY_CONDITIONS = { DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + DEVICE_CLASS_NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_DIOXIDE}], + DEVICE_CLASS_NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_MONOXIDE}], + DEVICE_CLASS_NITROUS_OXIDE: [{CONF_TYPE: CONF_IS_NITROUS_OXIDE}], + DEVICE_CLASS_OZONE: [{CONF_TYPE: CONF_IS_OZONE}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_IS_POWER_FACTOR}], + DEVICE_CLASS_PM1: [{CONF_TYPE: CONF_IS_PM1}], + DEVICE_CLASS_PM10: [{CONF_TYPE: CONF_IS_PM10}], + DEVICE_CLASS_PM25: [{CONF_TYPE: CONF_IS_PM25}], DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + DEVICE_CLASS_SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], @@ -83,12 +110,21 @@ CONDITION_SCHEMA = vol.All( CONF_IS_CO2, CONF_IS_CURRENT, CONF_IS_ENERGY, + CONF_IS_GAS, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, + CONF_IS_OZONE, + CONF_IS_NITROGEN_DIOXIDE, + CONF_IS_NITROGEN_MONOXIDE, + CONF_IS_NITROUS_OXIDE, CONF_IS_POWER, CONF_IS_POWER_FACTOR, + CONF_IS_PM1, + CONF_IS_PM10, + CONF_IS_PM25, CONF_IS_PRESSURE, CONF_IS_SIGNAL_STRENGTH, + CONF_IS_SULPHUR_DIOXIDE, CONF_IS_TEMPERATURE, CONF_IS_VOLTAGE, CONF_IS_VALUE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 3b00bae816d..2de09c01bc1 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -19,12 +19,21 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ) @@ -44,12 +53,21 @@ CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" CONF_CURRENT = "current" CONF_ENERGY = "energy" +CONF_GAS = "gas" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" +CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" +CONF_NITROGEN_MONOXIDE = "nitrogen_monoxide" +CONF_NITROUS_OXIDE = "nitrous_oxide" +CONF_OZONE = "ozone" +CONF_PM1 = "pm1" +CONF_PM10 = "pm10" +CONF_PM25 = "pm25" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" CONF_PRESSURE = "pressure" CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" CONF_VOLTAGE = "voltage" CONF_VALUE = "value" @@ -60,12 +78,21 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + DEVICE_CLASS_NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_NITROGEN_DIOXIDE}], + DEVICE_CLASS_NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], + DEVICE_CLASS_NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], + DEVICE_CLASS_OZONE: [{CONF_TYPE: CONF_OZONE}], + DEVICE_CLASS_PM1: [{CONF_TYPE: CONF_PM1}], + DEVICE_CLASS_PM10: [{CONF_TYPE: CONF_PM10}], + DEVICE_CLASS_PM25: [{CONF_TYPE: CONF_PM25}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + DEVICE_CLASS_SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], @@ -83,12 +110,21 @@ TRIGGER_SCHEMA = vol.All( CONF_CO2, CONF_CURRENT, CONF_ENERGY, + CONF_GAS, CONF_HUMIDITY, CONF_ILLUMINANCE, + CONF_NITROGEN_DIOXIDE, + CONF_NITROGEN_MONOXIDE, + CONF_NITROUS_OXIDE, + CONF_OZONE, + CONF_PM1, + CONF_PM10, + CONF_PM25, CONF_POWER, CONF_POWER_FACTOR, CONF_PRESSURE, CONF_SIGNAL_STRENGTH, + CONF_SULPHUR_DIOXIDE, CONF_TEMPERATURE, CONF_VOLTAGE, CONF_VALUE, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index afcfe2f228d..2cbca09c09d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -11,11 +11,14 @@ from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_MONETARY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + STATE_CLASSES, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -35,25 +38,36 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, State -import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util +import homeassistant.util.volume as volume_util from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_OR_UNIT_STATISTICS = { - DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, - DEVICE_CLASS_MONETARY: {"sum"}, - DEVICE_CLASS_POWER: {"mean", "min", "max"}, - DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, - DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, - PERCENTAGE: {"mean", "min", "max"}, + STATE_CLASS_MEASUREMENT: { + DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, + DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, + DEVICE_CLASS_POWER: {"mean", "min", "max"}, + DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, + DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + PERCENTAGE: {"mean", "min", "max"}, + # Deprecated, support will be removed in Home Assistant 2021.11 + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, + }, + STATE_CLASS_TOTAL_INCREASING: { + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, + }, } # Normalized units which will be stored in the statistics table @@ -62,6 +76,7 @@ DEVICE_CLASS_UNITS = { DEVICE_CLASS_POWER: POWER_WATT, DEVICE_CLASS_PRESSURE: PRESSURE_PA, DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS, + DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { @@ -92,30 +107,39 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, TEMP_KELVIN: temperature_util.kelvin_to_celsius, }, + # Convert volume to cubic meter + DEVICE_CLASS_GAS: { + VOLUME_CUBIC_METERS: lambda x: x, + VOLUME_CUBIC_FEET: volume_util.cubic_feet_to_cubic_meter, + }, } # Keep track of entities for which a warning about unsupported unit has been logged WARN_UNSUPPORTED_UNIT = set() -def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: - """Get (entity_id, device_class) of all sensors for which to compile statistics.""" +def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str]]: + """Get (entity_id, state_class, key) of all sensors for which to compile statistics. + + Key is either a device class or a unit and is used to index the + DEVICE_CLASS_OR_UNIT_STATISTICS map. + """ all_sensors = hass.states.all(DOMAIN) entity_ids = [] for state in all_sensors: - if state.attributes.get(ATTR_STATE_CLASS) != STATE_CLASS_MEASUREMENT: + if (state_class := state.attributes.get(ATTR_STATE_CLASS)) not in STATE_CLASSES: continue if ( key := state.attributes.get(ATTR_DEVICE_CLASS) - ) in DEVICE_CLASS_OR_UNIT_STATISTICS: - entity_ids.append((state.entity_id, key)) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]: + entity_ids.append((state.entity_id, state_class, key)) if ( key := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - ) in DEVICE_CLASS_OR_UNIT_STATISTICS: - entity_ids.append((state.entity_id, key)) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]: + entity_ids.append((state.entity_id, state_class, key)) return entity_ids @@ -217,8 +241,8 @@ def compile_statistics( hass, start - datetime.timedelta.resolution, end, [i[0] for i in entities] ) - for entity_id, key in entities: - wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] + for entity_id, state_class, key in entities: + wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key] if entity_id not in history_list: continue @@ -249,39 +273,52 @@ def compile_statistics( stat["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: - last_reset = old_last_reset = None new_state = old_state = None _sum = 0 last_stats = statistics.get_last_statistics(hass, 1, entity_id) if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point - last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] new_state = old_state = last_stats[entity_id][0]["state"] _sum = last_stats[entity_id][0]["sum"] for fstate, state in fstates: - if "last_reset" not in state.attributes: + # Deprecated, will be removed in Home Assistant 2021.10 + if ( + "last_reset" not in state.attributes + and state_class == STATE_CLASS_MEASUREMENT + ): continue - if (last_reset := state.attributes["last_reset"]) != old_last_reset: + + reset = False + if old_state is None: + reset = True + elif state_class == STATE_CLASS_TOTAL_INCREASING and ( + old_state is None or (new_state is not None and fstate < new_state) + ): + reset = True + + if reset: # The sensor has been reset, update the sum if old_state is not None: _sum += new_state - old_state # ..and update the starting point new_state = fstate - old_last_reset = last_reset - old_state = new_state + # Force a new cycle to start at 0 + if old_state is not None: + old_state = 0.0 + else: + old_state = new_state else: new_state = fstate - if last_reset is None or new_state is None or old_state is None: + if new_state is None or old_state is None: # No valid updates result.pop(entity_id) continue # Update the sum with the last state _sum += new_state - old_state - stat["last_reset"] = dt_util.parse_datetime(last_reset) stat["sum"] = _sum stat["state"] = new_state @@ -296,8 +333,8 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - statistic_ids = {} - for entity_id, key in entities: - provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] + for entity_id, state_class, key in entities: + provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key] if statistic_type is not None and statistic_type not in provided_statistics: continue @@ -305,7 +342,11 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - state = hass.states.get(entity_id) assert state - if "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes: + if ( + "sum" in provided_statistics + and ATTR_LAST_RESET not in state.attributes + and state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + ): continue native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index efe5366cfec..431e8a4789a 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -5,11 +5,20 @@ "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", + "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", + "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", + "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", + "is_ozone": "Current {entity_name} ozone concentration level", + "is_pm1": "Current {entity_name} PM1 concentration level", + "is_pm10": "Current {entity_name} PM10 concentration level", + "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", + "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", @@ -21,11 +30,20 @@ "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", + "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", + "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", + "ozone": "{entity_name} ozone concentration changes", + "pm1": "{entity_name} PM1 concentration changes", + "pm10": "{entity_name} PM10 concentration changes", + "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", + "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index a30748d5c9c..9303635ca60 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}", "is_current": "Intensitat actual de {entity_name}", "is_energy": "Energia actual de {entity_name}", + "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", + "is_nitrogen_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de nitrogen de {entity_name}", + "is_nitrogen_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de nitrogen de {entity_name}", + "is_nitrous_oxide": "Concentraci\u00f3 actual d'\u00f2xid nitr\u00f3s de {entity_name}", + "is_ozone": "Concentraci\u00f3 actual d'oz\u00f3 de {entity_name}", + "is_pm1": "Concentraci\u00f3 actual de PM1 de {entity_name}", + "is_pm10": "Concentraci\u00f3 actual de PM10 de {entity_name}", + "is_pm25": "Concentraci\u00f3 actual de PM2.5 de {entity_name}", "is_power": "Pot\u00e8ncia actual de {entity_name}", "is_power_factor": "Factor de pot\u00e8ncia actual de {entity_name}", "is_pressure": "Pressi\u00f3 actual de {entity_name}", "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", + "is_sulphur_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de sofre de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", "is_value": "Valor actual de {entity_name}", "is_voltage": "Voltatge actual de {entity_name}" @@ -22,12 +31,21 @@ "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", "energy": "Canvia l'energia de {entity_name}", + "gas": "Canvia el gas de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", + "nitrogen_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de nitrogen de {entity_name}", + "nitrogen_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de nitrogen de {entity_name}", + "nitrous_oxide": "Canvia la concentraci\u00f3 d'\u00f2xid nitr\u00f3s de {entity_name}", + "ozone": "Canvia la concentraci\u00f3 d'oz\u00f3 de {entity_name}", + "pm1": "Canvia la concentraci\u00f3 de PM1 de {entity_name}", + "pm10": "Canvia la concentraci\u00f3 de PM10 de {entity_name}", + "pm25": "Canvia la concentraci\u00f3 de PM2.5 de {entity_name}", "power": "Canvia la pot\u00e8ncia de {entity_name}", "power_factor": "Canvia el factor de pot\u00e8ncia de {entity_name}", "pressure": "Canvia la pressi\u00f3 de {entity_name}", "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", + "sulphur_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de sofre de {entity_name}", "temperature": "Canvia la temperatura de {entity_name}", "value": "Canvia el valor de {entity_name}", "voltage": "Canvia el voltatge de {entity_name}" diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index dfa2a263783..f493f4134ed 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -4,6 +4,7 @@ "is_battery_level": "Aktu\u00e1ln\u00ed \u00farove\u0148 nabit\u00ed baterie {entity_name}", "is_current": "Aktu\u00e1ln\u00ed proud {entity_name}", "is_energy": "Aktu\u00e1ln\u00ed energie {entity_name}", + "is_gas": "Aktu\u00e1ln\u00ed mno\u017estv\u00ed plynu {entity_name}", "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", "is_illuminance": "Aktu\u00e1ln\u00ed osv\u011btlen\u00ed {entity_name}", "is_power": "Aktu\u00e1ln\u00ed v\u00fdkon {entity_name}", @@ -18,6 +19,7 @@ "battery_level": "P\u0159i zm\u011bn\u011b \u00farovn\u011b baterie {entity_name}", "current": "P\u0159i zm\u011bn\u011b proudu {entity_name}", "energy": "P\u0159i zm\u011bn\u011b energie {entity_name}", + "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 4f16c07be01..ed6b678480f 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", "is_current": "Aktueller Strom von {entity_name}", "is_energy": "Aktuelle Energie von {entity_name}", + "is_gas": "Aktuelles {entity_name} Gas", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", + "is_nitrogen_dioxide": "Aktuelle Stickstoffdioxid-Konzentration von {entity_name}", + "is_nitrogen_monoxide": "Aktuelle Stickstoffmonoxidkonzentration von {entity_name}", + "is_nitrous_oxide": "Aktuelle Lachgaskonzentration von {entity_name}", + "is_ozone": "Aktuelle Ozonkonzentration von {entity_name}", + "is_pm1": "Aktuelle PM1-Konzentrationswert von {entity_name}", + "is_pm10": "Aktuelle PM10-Konzentrationswert von {entity_name}", + "is_pm25": "Aktuelle PM2.5-Konzentration von {entity_name}", "is_power": "Aktuelle {entity_name} Leistung", "is_power_factor": "Aktueller Leistungsfaktor f\u00fcr {entity_name}", "is_pressure": "{entity_name} Druck", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", + "is_sulphur_dioxide": "Aktuelle Schwefeldioxid-Konzentration von {entity_name}", "is_temperature": "Aktuelle {entity_name} Temperatur", "is_value": "Aktueller {entity_name} Wert", "is_voltage": "Aktuelle Spannung von {entity_name}" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", "energy": "{entity_name} Energie\u00e4nderungen", + "gas": "{entity_name} Gas\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", + "nitrogen_dioxide": "\u00c4nderung der Stickstoffdioxidkonzentration bei {entity_name}", + "nitrogen_monoxide": "\u00c4nderung der Stickstoffmonoxid-Konzentration bei {entity_name}", + "nitrous_oxide": "\u00c4nderung der Lachgaskonzentration bei {entity_name}", + "ozone": "\u00c4nderung der Ozonkonzentration bei {entity_name}", + "pm1": "\u00c4nderung der PM1-Konzentration bei {entity_name}", + "pm10": "\u00c4nderung der PM10-Konzentration bei {entity_name}", + "pm25": "\u00c4nderung der PM2,5-Konzentration bei {entity_name}", "power": "{entity_name} Leistungs\u00e4nderungen", "power_factor": "{entity_name} Leistungsfaktor\u00e4nderung", "pressure": "{entity_name} Druck\u00e4nderungen", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", + "sulphur_dioxide": "\u00c4nderung der Schwefeldioxidkonzentration bei {entity_name}", "temperature": "{entity_name} Temperatur\u00e4nderungen", "value": "{entity_name} Wert\u00e4nderungen", "voltage": "{entity_name} Spannungs\u00e4nderungen" diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index f8f45f93309..5fa23a334cb 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", + "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", + "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", + "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", + "is_ozone": "Current {entity_name} ozone concentration level", + "is_pm1": "Current {entity_name} PM1 concentration level", + "is_pm10": "Current {entity_name} PM10 concentration level", + "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_power_factor": "Current {entity_name} power factor", "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", + "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_value": "Current {entity_name} value", "is_voltage": "Current {entity_name} voltage" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", + "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", + "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", + "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", + "ozone": "{entity_name} ozone concentration changes", + "pm1": "{entity_name} PM1 concentration changes", + "pm10": "{entity_name} PM10 concentration changes", + "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "power_factor": "{entity_name} power factor changes", "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", + "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "value": "{entity_name} value changes", "voltage": "{entity_name} voltage changes" diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index da96c7d92db..48c61f321a1 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}", "is_current": "Corriente actual de {entity_name}", "is_energy": "Energ\u00eda actual de {entity_name}", + "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", "is_illuminance": "Luminosidad actual de {entity_name}", "is_power": "Potencia actual de {entity_name}", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", + "gas": "Cambio de gas de {entity_name}", "humidity": "Cambios de humedad de {entity_name}", "illuminance": "Cambios de luminosidad de {entity_name}", "power": "Cambios de potencia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 4169e7b82db..f36391e1e44 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", "is_current": "Praegune {entity_name} voolutugevus", "is_energy": "Praegune {entity_name} v\u00f5imsus", + "is_gas": "Praegune {entity_name} gaas", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", + "is_nitrogen_dioxide": "Praegune {entity_name} l\u00e4mmastikdioksiidi kontsentratsioonitase", + "is_nitrogen_monoxide": "Praegune {entity_name} l\u00e4mmastikmonooksiidi kontsentratsioonitase", + "is_nitrous_oxide": "Praegune {entity_name} dil\u00e4mmastikoksiidi kontsentratsioonitase", + "is_ozone": "Praegune osoonisisalduse tase {entity_name}", + "is_pm1": "Praegune {entity_name} PM1 kontsentratsioonitase", + "is_pm10": "Praegune {entity_name} PM10 kontsentratsioonitase", + "is_pm25": "Praegune {entity_name} PM2.5 kontsentratsioonitase", "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", "is_power_factor": "Praegune {entity_name} v\u00f5imsusfaktor", "is_pressure": "Praegune {entity_name} r\u00f5hk", "is_signal_strength": "Praegune {entity_name} signaali tugevus", + "is_sulphur_dioxide": "Praegune v\u00e4\u00e4veldioksiidi kontsentratsioonitase {entity_name}", "is_temperature": "Praegune {entity_name} temperatuur", "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", "is_voltage": "Praegune {entity_name}pinge" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "energy": "{entity_name} v\u00f5imsus muutub", + "gas": "{entity_name} gaasivahetus", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", + "nitrogen_dioxide": "{entity_name} l\u00e4mmastikdioksiidi kontsentratsiooni muutused", + "nitrogen_monoxide": "{entity_name} l\u00e4mmastikmonooksiidi kontsentratsiooni muutused", + "nitrous_oxide": "{entity_name} l\u00e4mmastikoksiidi kontsentratsiooni muutused", + "ozone": "{entity_name} osooni kontsentratsiooni muutused", + "pm1": "{entity_name} PM1 kontsentratsiooni muutused", + "pm10": "{entity_name} PM10 kontsentratsiooni muutused", + "pm25": "{entity_name} PM2.5 kontsentratsiooni muutused", "power": "{entity_name} energiare\u017eiimi muutub", "power_factor": "{entity_name} v\u00f5imsus muutub", "pressure": "{entity_name} r\u00f5hk muutub", "signal_strength": "{entity_name} signaalitugevus muutub", + "sulphur_dioxide": "{entity_name} v\u00e4\u00e4veldioksiidi kontsentratsiooni muutused", "temperature": "{entity_name} temperatuur muutub", "value": "{entity_name} v\u00e4\u00e4rtus muutub", "voltage": "{entity_name} pingemuutub" diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 9b1c9bece82..58ecdea0f24 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", "is_current": "Jelenlegi {entity_name} \u00e1ram", "is_energy": "A jelenlegi {entity_name} energia", + "is_gas": "Jelenlegi {entity_name} g\u00e1z", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", + "is_nitrogen_dioxide": "Jelenlegi {entity_name} nitrog\u00e9n-dioxid-koncentr\u00e1ci\u00f3 szint", + "is_nitrogen_monoxide": "Jelenlegi {entity_name} nitrog\u00e9n-monoxid-koncentr\u00e1ci\u00f3 szint", + "is_nitrous_oxide": "Jelenlegi {entity_name} dinitrog\u00e9n-oxid-koncentr\u00e1ci\u00f3 szint", + "is_ozone": "Jelenlegi {entity_name} \u00f3zonkoncentr\u00e1ci\u00f3 szint", + "is_pm1": "Jelenlegi {entity_name} PM1 koncentr\u00e1ci\u00f3 szintje", + "is_pm10": "Jelenlegi {entity_name} PM10 koncentr\u00e1ci\u00f3 szintje", + "is_pm25": "Jelenlegi {entity_name} PM2.5 koncentr\u00e1ci\u00f3 szintje", "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", "is_power_factor": "A jelenlegi {entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151", "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", + "is_sulphur_dioxide": "A {entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3 jelenlegi szintje", "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke", "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", + "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", + "nitrogen_dioxide": "{entity_name} nitrog\u00e9n-dioxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "nitrogen_monoxide": "{entity_name} nitrog\u00e9n-monoxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "nitrous_oxide": "{entity_name} dinitrog\u00e9n-oxid koncentr\u00e1ci\u00f3ja v\u00e1ltozik", + "ozone": "{entity_name} \u00f3zonkoncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm1": "{entity_name} PM1 koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm10": "{entity_name} PM10 koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm25": "{entity_name} PM2.5 koncentr\u00e1ci\u00f3 v\u00e1ltozik", "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", "power_factor": "{entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151 megv\u00e1ltozik", "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", + "sulphur_dioxide": "{entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3v\u00e1ltoz\u00e1s", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik", "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik" diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 6ae19c201d7..7b9b483c024 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Livello attuale di concentrazione di monossido di carbonio in {entity_name}", "is_current": "Corrente attuale di {entity_name}", "is_energy": "Energia attuale di {entity_name}", + "is_gas": "Attuale gas di {entity_name}", "is_humidity": "Umidit\u00e0 attuale di {entity_name}", "is_illuminance": "Illuminazione attuale di {entity_name}", + "is_nitrogen_dioxide": "Attuale livello di concentrazione di biossido di azoto di {entity_name}", + "is_nitrogen_monoxide": "Attuale livello di concentrazione di monossido di azoto di {entity_name}", + "is_nitrous_oxide": "Attuale livello di concentrazione di ossidi di azoto di {entity_name}", + "is_ozone": "Attuale livello di concentrazione di ozono di {entity_name}", + "is_pm1": "Attuale livello di concentrazione di PM1 di {entity_name}", + "is_pm10": "Attuale livello di concentrazione di PM10 di {entity_name}", + "is_pm25": "Attuale livello di concentrazione di PM2.5 di {entity_name}", "is_power": "Alimentazione attuale di {entity_name}", "is_power_factor": "Fattore di potenza attuale di {entity_name}", "is_pressure": "Pressione attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", + "is_sulphur_dioxide": "Attuale livello di concentrazione di anidride solforosa di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", "is_value": "Valore attuale di {entity_name}", "is_voltage": "Tensione attuale di {entity_name}" @@ -22,12 +31,21 @@ "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "variazioni di corrente di {entity_name}", "energy": "variazioni di energia di {entity_name}", + "gas": "Variazioni di gas di {entity_name}", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", "illuminance": "variazioni dell'illuminazione di {entity_name}", + "nitrogen_dioxide": "Variazioni della concentrazione di biossido di azoto di {entity_name}", + "nitrogen_monoxide": "Variazioni della concentrazione di monossido di azoto di {entity_name}", + "nitrous_oxide": "Variazioni della concentrazione di ossidi di azoto di {entity_name}", + "ozone": "Variazioni della concentrazione di ozono di {entity_name}", + "pm1": "Variazioni della concentrazione di PM1 di {entity_name}", + "pm10": "Variazioni della concentrazione di PM10 di {entity_name}", + "pm25": "Variazioni della concentrazione di PM2.5 di {entity_name}", "power": "variazioni di alimentazione di {entity_name}", "power_factor": "variazioni del fattore di potenza di {entity_name}", "pressure": "variazioni della pressione di {entity_name}", "signal_strength": "variazioni della potenza del segnale di {entity_name}", + "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", "temperature": "variazioni di temperatura di {entity_name}", "value": "{entity_name} valori cambiati", "voltage": "variazioni di tensione di {entity_name}" diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 745e097c6ee..c3ab0bf5bfa 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", "is_energy": "Huidige {entity_name} energie", + "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_nitrogen_dioxide": "Huidige {entity_name} stikstofdioxideconcentratie", + "is_nitrogen_monoxide": "Huidige {entity_name} stikstofmonoxideconcentratie", + "is_nitrous_oxide": "Huidige {entity_name} distikstofmonoxideconcentratie", + "is_ozone": "Huidige {entity_name} ozonconcentratie", + "is_pm1": "Huidige {entity_name} PM1-concentratie", + "is_pm10": "Huidige {entity_name} PM10-concentratie", + "is_pm25": "Huidige {entity_name} PM2.5-concentratie", "is_power": "Huidige {entity_name}\nvermogen", "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", "is_value": "Huidige {entity_name} waarde", "is_voltage": "Huidige {entity_name} spanning" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", "energy": "{entity_name} energieveranderingen", + "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "nitrogen_dioxide": "{entity_name} stikstofdioxideconcentratieverandering", + "nitrogen_monoxide": "{entity_name} stikstofmonoxideconcentratieverandering", + "nitrous_oxide": "{entity_name} distikstofmonoxideconcentratieverandering", + "ozone": "{entity_name} ozonconcentratieveranderingen", + "pm1": "{entity_name} PM1-concentratieveranderingen", + "pm10": "{entity_name} PM10-concentratieveranderingen", + "pm25": "{entity_name} PM2.5-concentratieveranderingen", "power": "{entity_name} vermogen gewijzigd", "power_factor": "{entity_name} power factor verandert", "pressure": "{entity_name} druk gewijzigd", "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", "value": "{entity_name} waarde gewijzigd", "voltage": "{entity_name} voltage verandert" diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 02204a4a49a..9af00949510 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5", "is_current": "Gjeldende {entity_name} str\u00f8m", "is_energy": "Gjeldende {entity_name} effekt", + "is_gas": "Gjeldende {entity_name} gass", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_nitrogen_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for {entity_name}", + "is_nitrogen_monoxide": "Gjeldende {entity_name} nitrogenmonoksidkonsentrasjonsniv\u00e5", + "is_nitrous_oxide": "Gjeldende {entity_name} lystgasskonsentrasjonsniv\u00e5", + "is_ozone": "Gjeldende {entity_name} ozonkonsentrasjonsniv\u00e5", + "is_pm1": "Gjeldende {entity_name} PM1 konsentrasjonsniv\u00e5", + "is_pm10": "Gjeldende konsentrasjonsniv\u00e5 for {entity_name}", + "is_pm25": "Gjeldende {entity_name} PM2.5 konsentrasjonsniv\u00e5", "is_power": "Gjeldende {entity_name}-effekt", "is_power_factor": "Gjeldende {entity_name} effektfaktor", "is_pressure": "Gjeldende {entity_name} trykk", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_sulphur_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for svoveldioksid for {entity_name}", "is_temperature": "Gjeldende {entity_name} temperatur", "is_value": "Gjeldende {entity_name} verdi", "is_voltage": "Gjeldende {entity_name} spenning" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "energy": "{entity_name} effektendringer", + "gas": "{entity_name} gass endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", + "nitrogen_dioxide": "{entity_name} nitrogendioksidkonsentrasjonsendringer", + "nitrogen_monoxide": "{entity_name} nitrogenmonoksidkonsentrasjonsendringer", + "nitrous_oxide": "{entity_name} endringer i nitrogenoksidskonsentrasjonen", + "ozone": "{entity_name} ozonkonsentrasjonsendringer", + "pm1": "{entity_name} PM1 -konsentrasjon endres", + "pm10": "{entity_name} PM10 -konsentrasjon endres", + "pm25": "{entity_name} PM2.5 konsentrasjon endres", "power": "{entity_name} effektendringer", "power_factor": "{entity_name} effektfaktorendringer", "pressure": "{entity_name} trykk endringer", "signal_strength": "{entity_name} signalstyrkeendringer", + "sulphur_dioxide": "{entity_name} svoveldioksidkonsentrasjon endres", "temperature": "{entity_name} temperaturendringer", "value": "{entity_name} verdi endringer", "voltage": "{entity_name} spenningsendringer" diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index c44c9002fef..641ec453c51 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u0430\u0440\u043d\u043e\u0433\u043e \u0433\u0430\u0437\u0430", "is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "is_gas": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_nitrogen_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "is_nitrogen_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "is_nitrous_oxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", + "is_ozone": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043e\u0437\u043e\u043d\u0430", + "is_pm1": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM1", + "is_pm10": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM10", + "is_pm25": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM2.5", "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_power_factor": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_sulphur_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "gas": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0433\u0430\u0437\u0430", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "nitrogen_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "nitrogen_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "nitrous_oxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", + "ozone": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043e\u0437\u043e\u043d\u0430", + "pm1": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM1", + "pm10": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM10", + "pm25": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 PM2.5", "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "power_factor": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "sulphur_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index a15af383da6..52ab5878ba3 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41", "is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b", + "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", + "is_nitrogen_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_nitrogen_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_nitrous_oxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_ozone": "\u76ee\u524d {entity_name} \u81ed\u6c27\u6fc3\u5ea6\u72c0\u614b", + "is_pm1": "\u76ee\u524d {entity_name} PM1 \u6fc3\u5ea6\u72c0\u614b", + "is_pm10": "\u76ee\u524d {entity_name} PM10 \u6fc3\u5ea6\u72c0\u614b", + "is_pm25": "\u76ee\u524d {entity_name} PM2.5 \u6fc3\u5ea6\u72c0\u614b", "is_power": "\u76ee\u524d{entity_name}\u96fb\u529b", "is_power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578", "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", + "is_sulphur_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u72c0\u614b", "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", "is_value": "\u76ee\u524d{entity_name}\u503c", "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" @@ -22,12 +31,21 @@ "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", + "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", + "nitrogen_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "nitrogen_monoxide": "{entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "nitrous_oxide": "{entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "ozone": "{entity_name} \u81ed\u6c27\u6fc3\u5ea6\u8b8a\u5316", + "pm1": "{entity_name} PM1 \u6fc3\u5ea6\u8b8a\u5316", + "pm10": "{entity_name} PM10 \u6fc3\u5ea6\u8b8a\u5316", + "pm25": "{entity_name} PM2.5 \u6fc3\u5ea6\u8b8a\u5316", "power": "{entity_name}\u96fb\u529b\u8b8a\u66f4", "power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578\u8b8a\u66f4", "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", + "sulphur_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u8b8a\u5316", "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", "value": "{entity_name}\u503c\u8b8a\u66f4", "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4" diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 79188df18b1..df07c41449e 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -25,7 +25,10 @@ "event_custom_components": "Esem\u00e9nyek k\u00fcld\u00e9se egy\u00e9ni \u00f6sszetev\u0151kb\u0151l", "event_handled": "K\u00fcldj\u00f6n kezelt esem\u00e9nyeket", "event_third_party_packages": "K\u00fcldj\u00f6n esem\u00e9nyeket harmadik f\u00e9l csomagjaib\u00f3l", - "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st" + "logging_event_level": "A napl\u00f3szint\u0171 Sentry esem\u00e9ny regisztr\u00e1l\u00e1sa", + "logging_level": "A napl\u00f3szint\u0171 Sentry a napl\u00f3k t\u00f6red\u00e9keinek r\u00f6gz\u00edt\u00e9se", + "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st", + "tracing_sample_rate": "A mintav\u00e9teli sebess\u00e9g nyomon k\u00f6vet\u00e9se; 0,0 \u00e9s 1,0 k\u00f6z\u00f6tt (1,0 = 100%)" } } } diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 1e73ae9ac83..cbdba50b6b6 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -245,6 +245,6 @@ class SerialSensor(SensorEntity): return self._attributes @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index fd017661de2..9332f268308 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -71,12 +71,12 @@ class ParticulateMatterSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index acd71b7c9e7..261b2680499 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,17 +1,11 @@ """Support for Sesame, by CANDY HOUSE.""" -from typing import Callable +from __future__ import annotations import pysesame2 import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_DEVICE_ID, - CONF_API_KEY, - STATE_LOCKED, - STATE_UNLOCKED, -) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -20,9 +14,7 @@ ATTR_SERIAL_NO = "serial" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +def setup_platform(hass, config: ConfigType, add_entities, discovery_info=None): """Set up the Sesame platform.""" api_key = config.get(CONF_API_KEY) @@ -35,20 +27,20 @@ def setup_platform( class SesameDevice(LockEntity): """Representation of a Sesame device.""" - def __init__(self, sesame: object) -> None: + def __init__(self, sesame: pysesame2.Sesame) -> None: """Initialize the Sesame device.""" - self._sesame = sesame + self._sesame: pysesame2.Sesame = sesame # Cached properties from pysesame object. - self._device_id = None + self._device_id: str | None = None self._serial = None - self._nickname = None + self._nickname: str | None = None self._is_locked = False self._responsive = False self._battery = -1 @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device.""" return self._nickname @@ -62,11 +54,6 @@ class SesameDevice(LockEntity): """Return True if the device is currently locked, else False.""" return self._is_locked - @property - def state(self) -> str: - """Get the state of the device.""" - return STATE_LOCKED if self._is_locked else STATE_UNLOCKED - def lock(self, **kwargs) -> None: """Lock the device.""" self._sesame.lock() diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index ab0f0779656..44720db2fcb 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -125,7 +125,7 @@ class SeventeenTrackSummarySensor(SensorEntity): return f"Seventeentrack Packages {self._status}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -135,7 +135,7 @@ class SeventeenTrackSummarySensor(SensorEntity): return f"summary_{self._data.account_id}_{slugify(self._status)}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return "packages" @@ -211,7 +211,7 @@ class SeventeenTrackPackageSensor(SensorEntity): return f"Seventeentrack Package: {name}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index dd1b3a9d66d..96d62152830 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, STATE_ON, BinarySensorEntity, @@ -99,7 +100,7 @@ REST_SENSORS: Final = { ), "fwupdate": RestAttributeDescription( name="Firmware Update", - icon="mdi:update", + device_class=DEVICE_CLASS_UPDATE, value=lambda status, _: status["update"]["has_update"], default_enabled=False, extra_state_attributes=lambda status: { diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 49e33dfd5e1..5646086285d 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,6 +1,7 @@ """Constants for the Shelly integration.""" from __future__ import annotations +import re from typing import Final COAP: Final = "coap" @@ -11,6 +12,22 @@ REST: Final = "rest" CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 +FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") + +# Firmware 1.11.0 release date, this firmware supports light transition +LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226 + +# max light transition time in milliseconds +MAX_TRANSITION_TIME: Final = 5000 + +MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( + "SHBDUO-1", + "SHCB-1", + "SHDM-1", + "SHDM-2", + "SHRGBW2", + "SHVIN-1", +) # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC: Final = 18 @@ -92,6 +109,3 @@ 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 a1ce2e671d1..743dd07414e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -179,7 +179,6 @@ 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: str | None = None @dataclass @@ -285,7 +284,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/light.py b/homeassistant/components/shelly/light.py index 047a105a30f..86624410708 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -14,12 +14,14 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, + ATTR_TRANSITION, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, SUPPORT_EFFECT, + SUPPORT_TRANSITION, LightEntity, brightness_supported, ) @@ -37,9 +39,13 @@ from .const import ( COAP, DATA_CONFIG_ENTRY, DOMAIN, + FIRMWARE_PATTERN, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, + LIGHT_TRANSITION_MIN_FIRMWARE_DATE, + MAX_TRANSITION_TIME, + MODELS_SUPPORTING_LIGHT_TRANSITION, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, ) @@ -110,6 +116,14 @@ class ShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "effect"): self._supported_features |= SUPPORT_EFFECT + if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION: + match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw")) + if ( + match is not None + and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE + ): + self._supported_features |= SUPPORT_TRANSITION + @property def supported_features(self) -> int: """Supported features.""" @@ -261,6 +275,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity): supported_color_modes = self._supported_color_modes params: dict[str, Any] = {"turn": "on"} + if ATTR_TRANSITION in kwargs: + params["transition"] = min( + int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + ) + if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): @@ -312,7 +331,15 @@ class ShellyLight(ShellyBlockEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" - self.control_result = await self.set_state(turn="off") + params: dict[str, Any] = {"turn": "off"} + + if ATTR_TRANSITION in kwargs: + params["transition"] = min( + int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + ) + + self.control_result = await self.set_state(**params) + self.async_write_ha_state() async def set_light_mode(self, set_mode: str | None) -> bool: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 56e4f63bc75..13cf56d3b3d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,7 +1,6 @@ """Sensor for Shelly.""" from __future__ import annotations -from datetime import datetime from typing import Final, cast from homeassistant.components import sensor @@ -21,9 +20,8 @@ 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 LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS +from .const import SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -113,49 +111,43 @@ SENSORS: Final = { unit=ENERGY_KILO_WATT_HOUR, 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, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("emeter", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=LAST_RESET_NEVER, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=LAST_RESET_NEVER, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("light", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_MEASUREMENT, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, default_enabled=False, - last_reset=LAST_RESET_UPTIME, ), ("relay", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, 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, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("roller", "rollerEnergy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, 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, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", @@ -256,7 +248,7 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" return self.attribute_value @@ -266,21 +258,7 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): 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: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return cast(str, self._unit) @@ -289,7 +267,7 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Represent a shelly REST sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" return self.attribute_value @@ -299,7 +277,7 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): return self.description.state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return self.description.unit @@ -308,7 +286,7 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Represent a shelly sleeping sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" if self.block is not None: return self.attribute_value @@ -321,6 +299,6 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.description.state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return cast(str, self._unit) diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 2c8f468aaed..9388e26515a 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -11,6 +11,9 @@ }, "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} a(z) {host} c\u00edmen? \n\n A jelsz\u00f3val v\u00e9dett akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, miel\u0151tt folytatn\u00e1 a be\u00e1ll\u00edt\u00e1st.\n Az elemmel m\u0171k\u00f6d\u0151, jelsz\u00f3val nem v\u00e9dett eszk\u00f6z\u00f6k hozz\u00e1ad\u00e1sra ker\u00fclnek, amikor az eszk\u00f6z fel\u00e9bred, most manu\u00e1lisan \u00e9bresztheti fel az eszk\u00f6zt egy rajta l\u00e9v\u0151 gombbal, vagy v\u00e1rhat a k\u00f6vetkez\u0151 adatfriss\u00edt\u00e9sre." + }, "credentials": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index fa0fc2d3906..1423a3b9327 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -62,12 +62,12 @@ class ShodanSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/shopping_list/translations/zh-Hans.json b/homeassistant/components/shopping_list/translations/zh-Hans.json new file mode 100644 index 00000000000..fa498b4ff60 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u914d\u7f6e" + }, + "step": { + "user": { + "description": "\u60a8\u8981\u914d\u7f6e\u8d2d\u7269\u6e05\u5355\u5417\uff1f", + "title": "\u8d2d\u7269\u6e05\u5355" + } + } + }, + "title": "\u8d2d\u7269\u6e05\u5355" +} \ No newline at end of file diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index a894623db47..1b1e1427e51 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -108,7 +108,7 @@ class SHTSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -123,7 +123,7 @@ class SHTSensorTemperature(SHTSensor): _attr_device_class = DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.hass.config.units.temperature_unit @@ -141,7 +141,7 @@ class SHTSensorHumidity(SHTSensor): """Representation of a humidity sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 75c2a4f0f63..41fb3469293 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -149,7 +149,7 @@ class SigfoxDevice(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the payload of the last message.""" return self._state 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), } diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8c23e575cc3..6bf029ead6e 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.3"], + "requirements": ["simplisafe-python==11.0.4"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 149319cd5bd..c3f8d7c3ab0 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -34,9 +34,9 @@ class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_FAHRENHEIT + _attr_native_unit_of_measurement = TEMP_FAHRENHEIT @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - self._attr_state = self._sensor.temperature + self._attr_native_value = self._sensor.temperature diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 8a2deedc534..f7c1b5afd9d 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -1,18 +1,25 @@ { "config": { "abort": { + "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "still_awaiting_mfa": "M\u00e9g v\u00e1r az MFA e-mail kattint\u00e1sra", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "mfa": { + "description": "Ellen\u0151rizze e-mailj\u00e9ben a SimpliSafe linkj\u00e9t. A link ellen\u0151rz\u00e9se ut\u00e1n t\u00e9rjen vissza ide, \u00e9s fejezze be az integr\u00e1ci\u00f3 telep\u00edt\u00e9s\u00e9t.", + "title": "SimpliSafe t\u00f6bbt\u00e9nyez\u0151s hiteles\u00edt\u00e9s" + }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" }, + "description": "Hozz\u00e1f\u00e9r\u00e9se lej\u00e1rt vagy visszavont\u00e1k. Adja meg jelszav\u00e1t a fi\u00f3k \u00fajb\u00f3li \u00f6sszekapcsol\u00e1s\u00e1hoz.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { @@ -24,5 +31,15 @@ "title": "T\u00f6ltsd ki az adataid" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)" + }, + "title": "A SimpliSafe konfigur\u00e1l\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index bc82715ad63..acd8adf0792 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -19,7 +19,7 @@ "data": { "password": "Passord" }, - "description": "Din tilgang har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.", + "description": "Tilgangen din har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din til p\u00e5 nytt.", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 3fe7aedfbb0..819f9c7147c 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -121,7 +121,7 @@ class SimulatedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -131,7 +131,7 @@ class SimulatedSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index f301100fa6c..ed0e8b8645f 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -64,10 +64,29 @@ def process_turn_on_params( if not supported_features & SUPPORT_TONES: params.pop(ATTR_TONE, None) - elif (tone := params.get(ATTR_TONE)) is not None and ( - not siren.available_tones or tone not in siren.available_tones - ): - raise ValueError(f"Invalid tone received for entity {siren.entity_id}: {tone}") + elif (tone := params.get(ATTR_TONE)) is not None: + # Raise an exception if the specified tone isn't available + is_tone_dict_value = bool( + isinstance(siren.available_tones, dict) + and tone in siren.available_tones.values() + ) + if ( + not siren.available_tones + or tone not in siren.available_tones + and not is_tone_dict_value + ): + raise ValueError( + f"Invalid tone specified for entity {siren.entity_id}: {tone}, " + "check the available_tones attribute for valid tones to pass in" + ) + + # If available tones is a dict, and the tone provided is a dict value, we need + # to transform it to the corresponding dict key before returning + if is_tone_dict_value: + assert isinstance(siren.available_tones, dict) + params[ATTR_TONE] = next( + key for key, value in siren.available_tones.items() if value == tone + ) if not supported_features & SUPPORT_DURATION: params.pop(ATTR_DURATION, None) @@ -131,7 +150,7 @@ class SirenEntity(ToggleEntity): """Representation of a siren device.""" entity_description: SirenEntityDescription - _attr_available_tones: list[int | str] | None = None + _attr_available_tones: list[int | str] | dict[int, str] | None = None @final @property @@ -145,7 +164,7 @@ class SirenEntity(ToggleEntity): return None @property - def available_tones(self) -> list[int | str] | None: + def available_tones(self) -> list[int | str] | dict[int, str] | None: """ Return a list of available tones. diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml index 8c5ed3be974..18bf782eaf2 100644 --- a/homeassistant/components/siren/services.yaml +++ b/homeassistant/components/siren/services.yaml @@ -7,7 +7,7 @@ turn_on: domain: siren fields: tone: - description: The tone to emit when turning the siren on. Must be supported by the integration. + description: The tone to emit when turning the siren on. When `available_tones` property is a map, either the key or the value can be used. Must be supported by the integration. example: fire required: false selector: diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 5b6eae96a7e..a72e1372ca0 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SkybeaconHumid(SensorEntity): """Representation of a Skybeacon humidity sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, name, mon): """Initialize a sensor.""" @@ -78,7 +78,7 @@ class SkybeaconHumid(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.mon.data["humid"] @@ -92,7 +92,7 @@ class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, name, mon): """Initialize a sensor.""" @@ -105,7 +105,7 @@ class SkybeaconTemp(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.mon.data["temp"] diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 87dc3c0bf8d..20e93fb90f7 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -1,4 +1,6 @@ """Camera support for the Skybell HD Doorbell.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -75,7 +77,9 @@ class SkybellCamera(SkybellDevice, Camera): return self._device.activity_image return self._device.image - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get the latest camera image.""" super().update() diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index cee864911b4..0ac26c1c76b 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -71,4 +71,4 @@ class SkybellSensor(SkybellDevice, SensorEntity): super().update() if self.entity_description.key == "chime_level": - self._attr_state = self._device.outdoor_chime_level + self._attr_native_value = self._device.outdoor_chime_level diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 8f5c17dad89..eec096e56c2 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -37,7 +37,7 @@ class SleepNumberSensor(SleepIQSensor, SensorEntity): self.update() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 6f3f7c2dca9..8808272ad75 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -32,7 +32,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util import dt as dt_util from .const import ( CONF_CUSTOM, @@ -165,9 +164,8 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self._device_info = device_info if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._attr_device_class = DEVICE_CLASS_ENERGY - self._attr_last_reset = dt_util.utc_from_timestamp(0) # Set sensor enabled to False. # Will be enabled by async_added_to_hass if actually used. @@ -179,12 +177,12 @@ class SMAsensor(CoordinatorEntity, SensorEntity): return self._sensor.name @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._sensor.value @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._sensor.unit diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 1037d399e64..94c5bbcdcac 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -37,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Smappee component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index c7f30a8b954..fb879e3cef5 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -282,7 +282,7 @@ class SmappeeSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -292,7 +292,7 @@ class SmappeeSensor(SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 5d3e65bb6fc..5b00dffde9c 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,8 +2,10 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rj\u00fck, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket, miel\u0151tt konfigur\u00e1lja a felh\u0151alap\u00fa eszk\u00f6zt.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_mdns": "Nem t\u00e1mogatott eszk\u00f6z a Smappee integr\u00e1ci\u00f3hoz.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, @@ -12,15 +14,21 @@ "environment": { "data": { "environment": "K\u00f6rnyezet" - } + }, + "description": "\u00c1ll\u00edtsa be a Smappee k\u00e9sz\u00fcl\u00e9ket az HomeAssistant-al val\u00f3 integr\u00e1ci\u00f3hoz." }, "local": { "data": { "host": "Hoszt" - } + }, + "description": "Adja meg a gazdag\u00e9pet a Smappee helyi integr\u00e1ci\u00f3j\u00e1nak elind\u00edt\u00e1s\u00e1hoz" }, "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni a \"{serialnumber} serialnumber}\" sorozatsz\u00e1m\u00fa Smappee -eszk\u00f6zt az HomeAssistanthoz?", + "title": "Felfedezett Smappee eszk\u00f6z" } } } diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 3e88221851b..7b500ed58e7 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -94,7 +94,7 @@ class SmartMeterTexasData: self.account = account websession = aiohttp_client.async_get_clientsession(hass) self.client = Client(websession, account) - self.meters = [] + self.meters: list = [] async def setup(self): """Fetch all of the user's meters.""" diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index f63edcce0fc..6914d3ef1ac 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: """Initialize the sensor.""" @@ -58,7 +58,7 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): return self._available @property - def state(self): + def native_value(self): """Get the latest reading.""" return self._state diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index ec4d2c9cad6..06d4de36b3c 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType DOMAIN = "smarthab" DATA_HUB = "hub" @@ -32,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SmartHab platform.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json index 222c95bba16..2e3cf430a9f 100644 --- a/homeassistant/components/smarthab/translations/hu.json +++ b/homeassistant/components/smarthab/translations/hu.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "service": "Hiba t\u00f6rt\u00e9nt a SmartHab el\u00e9r\u00e9se k\u00f6zben. A szolg\u00e1ltat\u00e1s le\u00e1llhat. Ellen\u0151rizze a kapcsolatot.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -10,7 +11,8 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet." + "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet.", + "title": "A SmartHab be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index bc64b173f20..fef2917fb8d 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -57,7 +57,7 @@ from .smartapp import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the SmartThings platform.""" await setup_smartapp_endpoint(hass) return True diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index cb8fa4bb6d2..7c682486f04 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -3,12 +3,15 @@ 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 STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, @@ -33,7 +36,6 @@ 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 @@ -133,7 +135,7 @@ CAPABILITY_TO_SENSORS = { "Energy Meter", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) ], Capability.equivalent_carbon_dioxide_measurement: [ @@ -492,7 +494,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{self._attribute}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.status.attributes[self._attribute].value @@ -502,18 +504,11 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" 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.""" @@ -534,7 +529,7 @@ class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{THREE_AXIS_NAMES[self._index]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" three_axis = self._device.status.attributes[Attribute.three_axis].value try: @@ -554,8 +549,9 @@ 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 + if self.report_name != "power": + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @property def name(self) -> str: @@ -568,7 +564,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{self.report_name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = self._device.status.attributes[Attribute.power_consumption].value if value is None or value.get(self.report_name) is None: @@ -585,15 +581,8 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): return DEVICE_CLASS_ENERGY @property - def unit_of_measurement(self): + def native_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 - - @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/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index bd6808db322..05e99bef2ea 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "A Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\n K\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok] szerint ({component_url}), ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "no_available_locations": "Nincsenek be\u00e1ll\u00edthat\u00f3 SmartThings helyek a Home Assistant alkalmaz\u00e1sban." + }, "error": { "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rlek pr\u00f3b\u00e1ld \u00fajra.", "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", @@ -8,16 +12,22 @@ "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." }, "step": { + "authorize": { + "title": "HomeAssistant enged\u00e9lyez\u00e9se" + }, "pat": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban." + "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", + "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" }, "select_location": { "data": { "location_id": "Elhelyezked\u00e9s" - } + }, + "description": "K\u00e9rj\u00fck, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n \u00faj ablakot nyitunk, \u00e9s megk\u00e9rj\u00fck, hogy jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", + "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", diff --git a/homeassistant/components/smartthings/translations/zh-Hans.json b/homeassistant/components/smartthings/translations/zh-Hans.json index 849d69d55e5..3db5d7f0354 100644 --- a/homeassistant/components/smartthings/translations/zh-Hans.json +++ b/homeassistant/components/smartthings/translations/zh-Hans.json @@ -8,6 +8,11 @@ "webhook_error": "SmartThings \u65e0\u6cd5\u9a8c\u8bc1 `base_url` \u4e2d\u914d\u7f6e\u7684\u7aef\u70b9\u3002\u8bf7\u67e5\u770b\u7ec4\u4ef6\u9700\u6c42\u3002" }, "step": { + "pat": { + "data": { + "access_token": "\u8bbf\u95ee\u4ee4\u724c" + } + }, "user": { "description": "\u8bf7\u8f93\u5165\u6309\u7167[\u8bf4\u660e]({component_url})\u521b\u5efa\u7684 SmartThings [\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c]({token_url})\u3002", "title": "\u8f93\u5165\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c" diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 95a862502cd..9922792ba12 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -87,7 +87,7 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity): """Generic class for SmartTub status sensors.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" if isinstance(self._state, Enum): return self._state.name.lower() @@ -109,7 +109,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): return self._state @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" return self.cycle.status.name.lower() @@ -147,7 +147,7 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): return self._state @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" return self.cycle.status.name.lower() diff --git a/homeassistant/components/smarttub/translations/zh-Hans.json b/homeassistant/components/smarttub/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/smarttub/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index b958185f9bd..a76e4b0f567 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -64,12 +64,12 @@ class SmartySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement 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) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index fc2310426e3..d405c817656 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -3,7 +3,7 @@ import logging import gammu # pylint: disable=import-error -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS from .const import DOMAIN, SMS_GATEWAY @@ -14,48 +14,40 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the GSM Signal Sensor sensor.""" gateway = hass.data[DOMAIN][SMS_GATEWAY] - entities = [] imei = await gateway.get_imei_async() - name = f"gsm_signal_imei_{imei}" - entities.append( - GSMSignalSensor( - hass, - gateway, - name, - ) + async_add_entities( + [ + GSMSignalSensor( + hass, + gateway, + imei, + SensorEntityDescription( + key="signal", + name=f"gsm_signal_imei_{imei}", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_registry_enabled_default=False, + ), + ) + ], + True, ) - async_add_entities(entities, True) class GSMSignalSensor(SensorEntity): """Implementation of a GSM Signal sensor.""" - def __init__( - self, - hass, - gateway, - name, - ): + def __init__(self, hass, gateway, imei, description): """Initialize the GSM Signal sensor.""" + self._attr_device_info = { + "identifiers": {(DOMAIN, str(imei))}, + "name": "SMS Gateway", + } + self._attr_unique_id = str(imei) self._hass = hass self._gateway = gateway - self._name = name self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return SIGNAL_STRENGTH_DECIBELS - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_SIGNAL_STRENGTH + self.entity_description = description @property def available(self): @@ -63,7 +55,7 @@ class GSMSignalSensor(SensorEntity): return self._state is not None @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state["SignalStrength"] @@ -78,8 +70,3 @@ class GSMSignalSensor(SensorEntity): def extra_state_attributes(self): """Return the sensor attributes.""" return self._state - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 7de2bfb91e2..09bfe3856cc 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -155,12 +155,12 @@ class SnmpSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index 1f735da4995..a4cdd595f90 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -54,7 +54,7 @@ class SochainSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self.chainso.data.get("confirmed_balance") @@ -63,7 +63,7 @@ class SochainSensor(SensorEntity): ) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 872781bf19c..c9c7136fb94 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -10,7 +13,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.util import dt as dt_util from .models import SolarEdgeSensorEntityDescription @@ -40,9 +42,8 @@ SENSOR_TYPES = [ json_key="lifeTimeData", name="Lifetime energy", icon="mdi:solar-power", - last_reset=dt_util.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -51,7 +52,7 @@ SENSOR_TYPES = [ name="Energy this year", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -60,7 +61,7 @@ SENSOR_TYPES = [ name="Energy this month", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -69,7 +70,7 @@ SENSOR_TYPES = [ name="Energy today", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -78,7 +79,7 @@ SENSOR_TYPES = [ name="Current Power", icon="mdi:solar-power", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), SolarEdgeSensorEntityDescription( @@ -185,6 +186,6 @@ SENSOR_TYPES = [ name="Storage Level", entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ] diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 85e01a2d7ee..23aa269cf36 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -133,7 +133,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API overview sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -147,7 +147,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): return self.data_service.attributes @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data @@ -161,7 +161,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -173,7 +173,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_type, data_service) - self._attr_unit_of_measurement = data_service.unit + self._attr_native_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -181,7 +181,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -200,7 +200,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): """Initialize the power flow sensor.""" super().__init__(platform_name, description, data_service) - self._attr_unit_of_measurement = data_service.unit + self._attr_native_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -208,7 +208,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -219,7 +219,7 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" attr = self.data_service.attributes.get(self.entity_description.json_key) if attr and "soc" in attr: diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index 69e450f55ff..a1a14c76357 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -13,7 +13,8 @@ "user": { "data": { "api_key": "API kulcs", - "name": "Ennek az install\u00e1ci\u00f3nak a neve" + "name": "Ennek az install\u00e1ci\u00f3nak a neve", + "site_id": "A SolarEdge webhelyazonos\u00edt\u00f3ja" }, "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez" } diff --git a/homeassistant/components/solaredge/translations/zh-Hans.json b/homeassistant/components/solaredge/translations/zh-Hans.json index baf8c980cb7..7f5039e9f93 100644 --- a/homeassistant/components/solaredge/translations/zh-Hans.json +++ b/homeassistant/components/solaredge/translations/zh-Hans.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 SolarEdge API", + "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "site_not_active": "\u672a\u6fc0\u6d3b" + }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u7801" - } + "api_key": "API \u5bc6\u7801", + "name": "\u5b89\u88c5\u540d\u79f0", + "site_id": "SolarEdge \u7ad9\u70b9 ID" + }, + "title": "\u5b9a\u4e49\u672c\u6b21\u5b89\u88c5\u7684 API \u53c2\u6570" } } } diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 3f159ce4480..9d162e919f4 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -50,6 +50,7 @@ SENSOR_TYPES = { ELECTRIC_POTENTIAL_VOLT, "mdi:current-ac", None, + None, ], "current_DC_voltage": [ "dcvoltage", @@ -57,6 +58,7 @@ SENSOR_TYPES = { ELECTRIC_POTENTIAL_VOLT, "mdi:current-dc", None, + None, ], "current_frequency": [ "gridfrequency", @@ -285,7 +287,7 @@ class SolarEdgeSensor(SensorEntity): return f"{self._platform_name} ({self._name})" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @@ -305,7 +307,7 @@ class SolarEdgeSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index cced913222a..4267502e3ca 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -31,7 +31,7 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors = {} + self._errors: dict = {} def _host_in_configuration_exists(self, host) -> bool: """Return True if host exists in configuration.""" diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 85a1531090d..a6a35bad80c 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -82,7 +82,7 @@ class SolarlogSensor(SensorEntity): return f"{self.device_name} {self._label}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the state of the sensor.""" return self._unit_of_measurement @@ -92,7 +92,7 @@ class SolarlogSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json index dd0ea8033ae..23baa393942 100644 --- a/homeassistant/components/solarlog/translations/hu.json +++ b/homeassistant/components/solarlog/translations/hu.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { - "host": "Hoszt" - } + "host": "Hoszt", + "name": "A Solar-Log szenzorokhoz haszn\u00e1land\u00f3 el\u0151tag" + }, + "title": "Hat\u00e1rozza meg a Solar-Log kapcsolatot" } } } diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index d14cfea2501..f6a6f581e12 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,7 +2,7 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.6"], + "requirements": ["solax==0.2.8"], "codeowners": ["@squishykid"], "iot_class": "local_polling" } diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index e47f5c57802..7854142c32b 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -6,8 +6,23 @@ from solax import real_time_api from solax.inverter import InverterError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_PORT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + TEMP_CELSIUS, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -34,10 +49,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] for sensor, (idx, unit) in api.inverter.sensor_map().items(): + device_class = state_class = None if unit == "C": + device_class = DEVICE_CLASS_TEMPERATURE + state_class = STATE_CLASS_MEASUREMENT unit = TEMP_CELSIUS + elif unit == "kWh": + device_class = DEVICE_CLASS_ENERGY + state_class = STATE_CLASS_TOTAL_INCREASING + elif unit == "V": + device_class = DEVICE_CLASS_VOLTAGE + state_class = STATE_CLASS_MEASUREMENT + elif unit == "A": + device_class = DEVICE_CLASS_CURRENT + state_class = STATE_CLASS_MEASUREMENT + elif unit == "W": + device_class = DEVICE_CLASS_POWER + state_class = STATE_CLASS_MEASUREMENT + elif unit == "%": + device_class = DEVICE_CLASS_BATTERY + state_class = STATE_CLASS_MEASUREMENT uid = f"{serial}-{idx}" - devices.append(Inverter(uid, serial, sensor, unit)) + devices.append(Inverter(uid, serial, sensor, unit, state_class, device_class)) endpoint.sensors = devices async_add_entities(devices) @@ -75,16 +108,26 @@ class RealTimeDataEndpoint: class Inverter(SensorEntity): """Class for a sensor.""" - def __init__(self, uid, serial, key, unit): + def __init__( + self, + uid, + serial, + key, + unit, + state_class=None, + device_class=None, + ): """Initialize an inverter sensor.""" self.uid = uid self.serial = serial self.key = key self.value = None self.unit = unit + self._attr_state_class = state_class + self._attr_device_class = device_class @property - def state(self): + def native_value(self): """State of this inverter attribute.""" return self.value @@ -99,7 +142,7 @@ class Inverter(SensorEntity): return f"Solax {self.serial} {self.key}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.unit diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 4df12c9f8f5..948e8d1e1e1 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -30,7 +30,7 @@ class SomaSensor(SomaEntity, SensorEntity): """Representation of a Soma cover device.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def name(self): @@ -38,7 +38,7 @@ class SomaSensor(SomaEntity, SensorEntity): return self.device["name"] + " battery level" @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self.battery_state diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index d013cb49fdf..c3e572ebe0a 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Csak egy Soma-fi\u00f3k konfigur\u00e1lhat\u00f3.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.", "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", diff --git a/homeassistant/components/soma/translations/zh-Hans.json b/homeassistant/components/soma/translations/zh-Hans.json new file mode 100644 index 00000000000..51fbc254b7f --- /dev/null +++ b/homeassistant/components/soma/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5 SOMA Connect\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 1817ba3fd8c..9a0602cb592 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -30,7 +30,7 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): """Representation of a Somfy thermostat battery.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" @@ -43,6 +43,6 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): self._climate = Thermostat(self.device, self.coordinator.client) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._climate.get_battery() diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 5a2a1ee6ab5..3610a930022 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -26,11 +26,15 @@ }, "step": { "entity_config": { + "data": { + "reverse": "A bor\u00edt\u00f3 megfordult" + }, "description": "Konfigur\u00e1lja az \u201e {entity_id} \u201d be\u00e1ll\u00edt\u00e1sait", "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa" }, "init": { "data": { + "default_reverse": "A konfigur\u00e1latlan bor\u00edt\u00f3k alap\u00e9rtelmezett megford\u00edt\u00e1si \u00e1llapota", "entity_id": "Konfigur\u00e1ljon egy adott entit\u00e1st.", "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa." }, diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index d173d42eaf7..3f5ef275fef 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -85,7 +85,7 @@ class SonarrSensor(SonarrEntity, SensorEntity): self._attr_name = name self._attr_icon = icon self._attr_unique_id = f"{entry_id}_{key}" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._attr_entity_registry_enabled_default = enabled_default self.last_update_success = False @@ -134,7 +134,7 @@ class SonarrCommandsSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._commands) @@ -181,7 +181,7 @@ class SonarrDiskspaceSensor(SonarrSensor): return attrs @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" free = self._total_free / 1024 ** 3 return f"{free:.2f}" @@ -223,7 +223,7 @@ class SonarrQueueSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._queue) @@ -261,7 +261,7 @@ class SonarrSeriesSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._items) @@ -304,7 +304,7 @@ class SonarrUpcomingSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._upcoming) @@ -347,6 +347,6 @@ class SonarrWantedSensor(SonarrSensor): return attrs @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._total diff --git a/homeassistant/components/sonarr/translations/zh-Hans.json b/homeassistant/components/sonarr/translations/zh-Hans.json new file mode 100644 index 00000000000..265928213f5 --- /dev/null +++ b/homeassistant/components/sonarr/translations/zh-Hans.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "description": "Sonarr \u96c6\u6210\u9700\u8981\u624b\u52a8\u91cd\u65b0\u9a8c\u8bc1\uff1a{host}" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u94a5", + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u663e\u793a\u5373\u5c06\u5185\u5bb9\u7684\u5929\u6570", + "wanted_max_items": "\u5185\u5bb9\u663e\u793a\u6700\u5927\u6570\u91cf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index b542591b294..2053d2857c2 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -1,5 +1,4 @@ """The songpal component.""" -from collections import OrderedDict import voluptuous as vol @@ -7,6 +6,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import CONF_ENDPOINT, DOMAIN @@ -22,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["media_player"] -async def async_setup(hass: HomeAssistant, config: OrderedDict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up songpal environment.""" conf = config.get(DOMAIN) if conf is None: diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index f0219ea8cf0..ae3652683d4 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -138,6 +138,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a Sonos config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await hass.data[DATA_SONOS_DISCOVERY_MANAGER].async_shutdown() + hass.data.pop(DATA_SONOS) + hass.data.pop(DATA_SONOS_DISCOVERY_MANAGER) + return unload_ok + + class SonosDiscoveryManager: """Manage sonos discovery.""" @@ -151,6 +160,11 @@ class SonosDiscoveryManager: self.hosts = hosts self.discovery_lock = asyncio.Lock() + async def async_shutdown(self): + """Stop all running tasks.""" + await self._async_stop_event_listener() + self._stop_manual_heartbeat() + def _create_soco(self, ip_address: str, source: SoCoCreationSource) -> SoCo | None: """Create a soco instance and return if successful.""" if ip_address in self.data.discovery_ignored: @@ -171,15 +185,14 @@ class SonosDiscoveryManager: ) return None - async def _async_stop_event_listener(self, event: Event) -> None: + async def _async_stop_event_listener(self, event: Event | None = None) -> None: await asyncio.gather( - *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()), - return_exceptions=True, + *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()) ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() - def _stop_manual_heartbeat(self, event: Event) -> None: + def _stop_manual_heartbeat(self, event: Event | None = None) -> None: if self.data.hosts_heartbeat: self.data.hosts_heartbeat() self.data.hosts_heartbeat = None 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/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 9e5277819a7..1a13e6f55f4 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -45,7 +45,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Get the unit of measurement.""" return PERCENTAGE @@ -54,7 +54,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): await self.speaker.async_poll_battery() @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self.speaker.battery_info.get("Level") diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 434717f7a85..9485d5dcff3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -346,10 +346,13 @@ class SonosSpeaker: async def async_unsubscribe(self) -> None: """Cancel all subscriptions.""" _LOGGER.debug("Unsubscribing from events for %s", self.zone_name) - await asyncio.gather( + results = await asyncio.gather( *(subscription.unsubscribe() for subscription in self._subscriptions), return_exceptions=True, ) + for result in results: + if isinstance(result, Exception): + _LOGGER.debug("Unsubscribe failed for %s: %s", self.zone_name, result) self._subscriptions = [] @callback @@ -497,22 +500,25 @@ class SonosSpeaker: self.async_write_entity_states() async def async_unseen( - self, now: datetime.datetime | None = None, will_reconnect: bool = False + self, callback_timestamp: datetime.datetime | None = None ) -> None: """Make this player unavailable when it was not seen recently.""" if self._seen_timer: self._seen_timer() self._seen_timer = None - hostname = uid_to_short_hostname(self.soco.uid) - zcname = f"{hostname}.{MDNS_SERVICE}" - aiozeroconf = await zeroconf.async_get_async_instance(self.hass) - if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname): - # We can still see the speaker via zeroconf check again later. - self._seen_timer = self.hass.helpers.event.async_call_later( - SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen - ) - return + if callback_timestamp: + # Called by a _seen_timer timeout, check mDNS one more time + # This should not be checked in an "active" unseen scenario + hostname = uid_to_short_hostname(self.soco.uid) + zcname = f"{hostname}.{MDNS_SERVICE}" + aiozeroconf = await zeroconf.async_get_async_instance(self.hass) + if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname): + # We can still see the speaker via zeroconf check again later. + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + return _LOGGER.debug( "No activity and could not locate %s on the network. Marking unavailable", @@ -527,9 +533,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 +543,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 @@ -620,8 +641,8 @@ class SonosSpeaker: def async_update_groups(self, event: SonosEvent) -> None: """Handle callback for topology change event.""" if not hasattr(event, "zone_player_uui_ds_in_group"): - return None - self.hass.async_add_job(self.create_update_groups_coro(event)) + return + self.hass.async_create_task(self.create_update_groups_coro(event)) def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: """Handle callback for topology change event.""" diff --git a/homeassistant/components/sonos/translations/no.json b/homeassistant/components/sonos/translations/no.json index 2da0b5a1b0b..2e9b464f5f2 100644 --- a/homeassistant/components/sonos/translations/no.json +++ b/homeassistant/components/sonos/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_sonos_device": "Oppdaget enhet er ikke en Sonos -enhet", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "step": { diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 897ffa126fa..c9962362406 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -17,19 +17,19 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key="ping", name="Ping", - unit_of_measurement=TIME_MILLISECONDS, + native_unit_of_measurement=TIME_MILLISECONDS, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="download", name="Download", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="upload", name="Upload", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 1c6c80a6af1..2dc12c956de 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -85,7 +85,7 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): await super().async_added_to_hass() state = await self.async_get_last_state() if state: - self._attr_state = state.state + self._attr_native_value = state.state @callback def update() -> None: @@ -100,8 +100,12 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Update sensors state.""" if self.coordinator.data: if self.entity_description.key == "ping": - self._attr_state = self.coordinator.data["ping"] + self._attr_native_value = self.coordinator.data["ping"] elif self.entity_description.key == "download": - self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) + self._attr_native_value = round( + self.coordinator.data["download"] / 10 ** 6, 2 + ) elif self.entity_description.key == "upload": - self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2) + self._attr_native_value = round( + self.coordinator.data["upload"] / 10 ** 6, 2 + ) diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index ec08c711e1d..cd08c3bd2d6 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -14,6 +14,8 @@ "step": { "init": { "data": { + "manual": "Automatikus friss\u00edt\u00e9s letilt\u00e1sa", + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (perc)", "server_name": "V\u00e1laszd ki a teszt szervert" } } diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index 998a9ff8eee..8b38fdbe6f6 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -1,7 +1,9 @@ """Support for Spider Powerplugs (energy & power).""" -from datetime import datetime - -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -9,7 +11,6 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.util import dt as dt_util from .const import DOMAIN @@ -29,9 +30,9 @@ async def async_setup_entry(hass, config, async_add_entities): class SpiderPowerPlugEnergy(SensorEntity): """Representation of a Spider Power Plug (energy).""" - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__(self, api, power_plug) -> None: """Initialize the Spider Power Plug.""" @@ -59,17 +60,10 @@ class SpiderPowerPlugEnergy(SensorEntity): return f"{self.power_plug.name} Total Energy Today" @property - def state(self) -> float: + def native_value(self) -> float: """Return todays energy usage in Kwh.""" return round(self.power_plug.today_energy_consumption / 1000, 2) - @property - def last_reset(self) -> datetime: - """Return the time when last reset; Every midnight.""" - return dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - def update(self) -> None: """Get the latest data.""" self.power_plug = self.api.get_power_plug(self.power_plug.id) @@ -80,7 +74,7 @@ class SpiderPowerPlugPower(SensorEntity): _attr_device_class = DEVICE_CLASS_POWER _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, api, power_plug) -> None: """Initialize the Spider Power Plug.""" @@ -108,7 +102,7 @@ class SpiderPowerPlugPower(SensorEntity): return f"{self.power_plug.name} Power Consumption" @property - def state(self) -> float: + def native_value(self) -> float: """Return the current power usage in W.""" return round(self.power_plug.current_energy_consumption) diff --git a/homeassistant/components/spotify/translations/zh-Hans.json b/homeassistant/components/spotify/translations/zh-Hans.json index 19a6909de48..fdda1685cf1 100644 --- a/homeassistant/components/spotify/translations/zh-Hans.json +++ b/homeassistant/components/spotify/translations/zh-Hans.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", + "missing_configuration": "Spotify \u96c6\u6210\u672a\u914d\u7f6e \u3002\u8bf7\u9075\u5faa\u6587\u6863\u914d\u7f6e\u3002", + "no_url_available": "\u65e0 URL \u53ef\u7528\uff0c\u66f4\u591a\u4fe1\u606f\u8bf7[check the help section]({docs_url})", + "reauth_account_mismatch": "\u5df2\u9a8c\u8bc1\u7684 Spotify \u5e10\u6237\u4e0e\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u7684\u5e10\u6237\u4e0d\u5339\u914d\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u901a\u8fc7 Spotify \u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u9009\u62e9\u9a8c\u8bc1\u65b9\u5f0f" + }, + "reauth_confirm": { + "description": "Spotify \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u5e10\u6237\uff1a {account}" + } + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u53ef\u8bbf\u95ee Spotify API" diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 4c1c29b82a6..1b0ae5a9076 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -123,12 +123,12 @@ class SQLSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the query's current state.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4c0ec186707..294a1105a71 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -137,7 +137,6 @@ async def build_item_response(entity, player, payload): async def library_payload(player): """Create response payload to describe contents of library.""" - library_info = { "title": "Music Library", "media_class": MEDIA_CLASS_DIRECTORY, diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 1f1c23942db..4b05588e281 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -5,8 +5,9 @@ import logging from pysqueezebox import Server, async_discover import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,6 +16,8 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity_registry import async_get from .const import DEFAULT_PORT, DOMAIN @@ -166,28 +169,18 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason=error) return self.async_create_entry(title=config[CONF_HOST], data=config) - async def async_step_discovery(self, discovery_info): - """Handle discovery.""" - _LOGGER.debug("Reached discovery flow with info: %s", discovery_info) + async def async_step_integration_discovery(self, discovery_info): + """Handle discovery of a server.""" + _LOGGER.debug("Reached server discovery flow with info: %s", discovery_info) if "uuid" in discovery_info: await self.async_set_unique_id(discovery_info.pop("uuid")) self._abort_if_unique_id_configured() else: # attempt to connect to server and determine uuid. will fail if # password required - - if CONF_HOST not in discovery_info and IP_ADDRESS in discovery_info: - discovery_info[CONF_HOST] = discovery_info[IP_ADDRESS] - - if CONF_PORT not in discovery_info: - discovery_info[CONF_PORT] = DEFAULT_PORT - error = await self._validate_input(discovery_info) if error: - if MAC_ADDRESS in discovery_info: - await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) - else: - await self._async_handle_discovery_without_unique_id() + await self._async_handle_discovery_without_unique_id() # update schema with suggested values from discovery self.data_schema = _base_schema(discovery_info) @@ -195,3 +188,23 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}}) return await self.async_step_edit() + + async def async_step_dhcp(self, discovery_info): + """Handle dhcp discovery of a Squeezebox player.""" + _LOGGER.debug( + "Reached dhcp discovery of a player with info: %s", discovery_info + ) + await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Configuring dhcp player with unique id: %s", self.unique_id) + + registry = async_get(self.hass) + + # if we have detected this player, do nothing. if not, there must be a server out there for us to configure, so start the normal user flow (which tries to autodetect server) + if registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.unique_id) is not None: + # this player is already known, so do nothing other than mark as configured + raise data_entry_flow.AbortFlow("already_configured") + + # if the player is unknown, then we likely need to configure its server + return await self.async_step_user() diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index baf8a011c65..1ba406097d7 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -27,7 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import SOURCE_DISCOVERY +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY from homeassistant.const import ( ATTR_COMMAND, CONF_HOST, @@ -43,6 +43,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -127,7 +128,7 @@ async def start_server_discovery(hass): asyncio.create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ CONF_HOST: server.host, CONF_PORT: int(server.port), @@ -146,7 +147,6 @@ async def start_server_discovery(hass): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up squeezebox platform from platform entry in configuration.yaml (deprecated).""" - if config: await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config @@ -283,7 +283,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): @property def unique_id(self): """Return a unique ID.""" - return self._player.player_id + return format_mac(self._player.player_id) @property def available(self): @@ -573,7 +573,6 @@ class SqueezeBoxEntity(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" - _LOGGER.debug( "Reached async_browse_media with content_type %s and content_id %s", media_content_type, diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json index e9d7413ebfa..a047dbca45f 100644 --- a/homeassistant/components/squeezebox/translations/hu.json +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -18,7 +18,8 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Kapcsolati inform\u00e1ci\u00f3k szerkeszt\u00e9se" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 6973c58600e..97b65840e83 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -93,14 +93,14 @@ class SrpEntity(SensorEntity): return self.type @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._state: return f"{self._state:.2f}" return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json index 9ade185d831..4d617e09cfc 100644 --- a/homeassistant/components/srp_energy/translations/hu.json +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -13,6 +13,7 @@ "user": { "data": { "id": "A fi\u00f3k azonos\u00edt\u00f3ja", + "is_tou": "A haszn\u00e1lati id\u0151 terv", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/srp_energy/translations/zh-Hans.json b/homeassistant/components/srp_energy/translations/zh-Hans.json new file mode 100644 index 00000000000..36016f3e217 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 31ebb0d1a92..1fd2bba77cc 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -9,6 +9,7 @@ import logging from typing import Any, Callable from async_upnp_client.search import SSDPListener +from async_upnp_client.ssdp import SSDP_PORT from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries @@ -115,14 +116,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@core_callback -def _async_use_default_interface(adapters: list[network.Adapter]) -> bool: - for adapter in adapters: - if adapter["enabled"] and not adapter["default"]: - return False - return True - - @core_callback def _async_process_callbacks( callbacks: list[Callable[[dict], None]], discovery_info: dict[str, str] @@ -203,30 +196,29 @@ class Scanner: """Build the list of ssdp sources.""" adapters = await network.async_get_adapters(self.hass) sources: set[IPv4Address | IPv6Address] = set() - if _async_use_default_interface(adapters): + if network.async_only_default_interface_enabled(adapters): sources.add(IPv4Address("0.0.0.0")) return sources - for adapter in adapters: - if not adapter["enabled"]: - continue - if adapter["ipv4"]: - ipv4 = adapter["ipv4"][0] - sources.add(IPv4Address(ipv4["address"])) - if adapter["ipv6"]: - ipv6 = adapter["ipv6"][0] - # With python 3.9 add scope_ids can be - # added by enumerating adapter["ipv6"]s - # IPv6Address(f"::%{ipv6['scope_id']}") - sources.add(IPv6Address(ipv6["address"])) + return { + source_ip + for source_ip in await network.async_get_enabled_source_ips(self.hass) + if not source_ip.is_loopback + and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + } - return sources - - @core_callback - def async_scan(self, *_: Any) -> None: - """Scan for new entries.""" + async def async_scan(self, *_: Any) -> None: + """Scan for new entries using ssdp default and broadcast target.""" for listener in self._ssdp_listeners: listener.async_search() + try: + IPv4Address(listener.source_ip) + except ValueError: + continue + # Some sonos devices only seem to respond if we send to the broadcast + # address. This matches pysonos' behavior + # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 + listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) async def async_start(self) -> None: """Start the scanner.""" @@ -235,21 +227,9 @@ class Scanner: for source_ip in await self._async_build_source_set(): self._ssdp_listeners.append( SSDPListener( - async_callback=self._async_process_entry, source_ip=source_ip - ) - ) - try: - IPv4Address(source_ip) - except ValueError: - continue - # Some sonos devices only seem to respond if we send to the broadcast - # address. This matches pysonos' behavior - # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 - self._ssdp_listeners.append( - SSDPListener( + async_connect_callback=self.async_scan, async_callback=self._async_process_entry, source_ip=source_ip, - target_ip=IPV4_BROADCAST, ) ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 432686d9027..746e90c7388 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "async-upnp-client==0.19.1" + "async-upnp-client==0.20.0" ], "dependencies": ["network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index e7996befad3..92c6acbab0b 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -69,7 +69,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._key == "battery": return self._device.battery_level @@ -90,7 +90,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Get the unit of measurement.""" if self._key == "balance": return self._device.balance.get("currency") or "₽" diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 77f5ab307cb..ae1ac2d4987 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -79,12 +79,12 @@ class StarlingBalanceSensor(SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._starling_account.currency diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 661e00ed494..d4124ec3d7c 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -93,12 +93,12 @@ class StartcaSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index de32603c207..ea90346fe7c 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -203,12 +203,12 @@ class StatisticsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.mean if not self.is_binary else self.count @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement if not self.is_binary else None diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 45ae1a6c70a..18f7c6cc447 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -99,7 +99,7 @@ class SteamSensor(SensorEntity): return f"sensor.steam_{self._account}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 69def43b2a2..039163c6cf5 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -225,7 +225,7 @@ class PeekIterator(Iterator): 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: + if not self._buffer: self._next = self._iterator.__next__ def _pop_buffer(self) -> av.Packet: diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index ba722d0a4f2..3af87b8a3f8 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -87,12 +87,12 @@ class StreamLabsDailyUsage(SensorEntity): return WATER_ICON @property - def state(self): + def native_value(self): """Return the current daily usage.""" return self._streamlabs_usage_data.get_daily_usage() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return gallons as the unit measurement for water.""" return VOLUME_GALLONS @@ -110,7 +110,7 @@ class StreamLabsMonthlyUsage(StreamLabsDailyUsage): return f"{self._location_name} {NAME_MONTHLY_USAGE}" @property - def state(self): + def native_value(self): """Return the current monthly usage.""" return self._streamlabs_usage_data.get_monthly_usage() @@ -124,6 +124,6 @@ class StreamLabsYearlyUsage(StreamLabsDailyUsage): return f"{self._location_name} {NAME_YEARLY_USAGE}" @property - def state(self): + def native_value(self): """Return the current yearly usage.""" return self._streamlabs_usage_data.get_yearly_usage() diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 694ddeff998..3b5efbcba9c 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -17,6 +17,7 @@ import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_prepare_setup_platform from .const import ( @@ -34,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up STT.""" providers = {} diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index ff1d8b715d7..7aeab66b929 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -203,7 +203,7 @@ class SubaruSensor(SubaruEntity, SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" self.current_value = self.get_current_value() @@ -238,7 +238,7 @@ class SubaruSensor(SubaruEntity, SensorEntity): return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" if self.api_unit in TEMPERATURE_UNITS: return self.hass.config.units.temperature_unit diff --git a/homeassistant/components/subaru/translations/zh-Hans.json b/homeassistant/components/subaru/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/subaru/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 7170e0b8a67..c9c125e8e7e 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -62,12 +62,12 @@ class SuezSensor(SensorEntity): return COMPONENT_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return VOLUME_LITERS diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index f701df2d6c3..5db8680f1c9 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -50,7 +50,7 @@ class SupervisorProcessSensor(SensorEntity): return self._info.get("name") @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._info.get("statename") diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index e9a2c5b73a1..87a3260fc40 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_FLAP_ID, @@ -62,7 +63,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Sure Petcare integration.""" conf = config[DOMAIN] hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index fbc8222f292..922bfa84515 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -60,7 +60,7 @@ class SureBattery(SensorEntity): self._attr_device_class = DEVICE_CLASS_BATTERY self._attr_name = f"{surepy_entity.type.name.capitalize()} {surepy_entity.name.capitalize()} Battery Level" - self._attr_unit_of_measurement = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._attr_unique_id = ( f"{surepy_entity.household_id}-{surepy_entity.id}-battery" ) @@ -75,11 +75,11 @@ class SureBattery(SensorEntity): try: per_battery_voltage = state["battery"] / 4 voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - self._attr_state = min( + self._attr_native_value = min( int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100 ) except (KeyError, TypeError): - self._attr_state = None + self._attr_native_value = None if state: voltage_per_battery = float(state["battery"]) / 4 diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 1d77410f031..3daa7161869 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -94,14 +94,14 @@ class SwissHydrologicalDataSensor(SensorEntity): return f"{self._station}_{self._condition}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" if self._state is not None: return self.hydro_data.data["parameters"][self._condition]["unit"] return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if isinstance(self._state, (int, float)): return round(self._state, 2) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index a971524c22b..0f0ac28d530 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -84,7 +84,7 @@ class SwissPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self._opendata.connections[0]["departure"] diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 6c13067cd7f..6a23f1bb453 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import ( update_coordinator, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_DEVICE_PASSWORD, @@ -49,7 +50,7 @@ CCONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the switcher component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 705c6f0a2b6..e070bd52d0d 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -110,7 +110,7 @@ class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): # Entity class attributes self._attr_name = f"{wrapper.name} {description.name}" self._attr_icon = description.icon - self._attr_unit_of_measurement = description.unit + self._attr_native_unit_of_measurement = description.unit self._attr_device_class = description.device_class self._attr_entity_registry_enabled_default = description.default_enabled @@ -122,6 +122,6 @@ class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return] diff --git a/homeassistant/components/switcher_kis/translations/hu.json b/homeassistant/components/switcher_kis/translations/hu.json new file mode 100644 index 00000000000..c3be866fb85 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1sokat?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/no.json b/homeassistant/components/switcher_kis/translations/no.json new file mode 100644 index 00000000000..b3d6b5d782e --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 5e8ea2f88c2..924f8aaf669 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -105,7 +105,7 @@ class FolderSensor(SensorEntity): return f"{self._short_server_id}-{self._folder_id}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state["state"] diff --git a/homeassistant/components/syncthing/translations/zh-Hans.json b/homeassistant/components/syncthing/translations/zh-Hans.json new file mode 100644 index 00000000000..87d3db5c83f --- /dev/null +++ b/homeassistant/components/syncthing/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8ba4\u8bc1\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "title": "\u8bbe\u7f6e Syncthing \u96c6\u6210", + "token": "\u4ee4\u724c", + "url": "\u8fde\u63a5\u5730\u5740", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 2b559e0a15f..cc832f77f0a 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -124,7 +124,7 @@ class SyncThruSensor(CoordinatorEntity, SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measuremnt.""" return self._unit_of_measurement @@ -148,7 +148,7 @@ class SyncThruMainSensor(SyncThruSensor): self._id_suffix = "_main" @property - def state(self): + def native_value(self): """Set state to human readable version of syncthru status.""" return SYNCTHRU_STATE_HUMAN[self.syncthru.device_status()] @@ -182,7 +182,7 @@ class SyncThruTonerSensor(SyncThruSensor): return self.syncthru.toner_status().get(self._color, {}) @property - def state(self): + def native_value(self): """Show amount of remaining toner.""" return self.syncthru.toner_status().get(self._color, {}).get("remaining") @@ -204,7 +204,7 @@ class SyncThruDrumSensor(SyncThruSensor): return self.syncthru.drum_status().get(self._color, {}) @property - def state(self): + def native_value(self): """Show amount of remaining drum.""" return self.syncthru.drum_status().get(self._color, {}).get("remaining") @@ -225,7 +225,7 @@ class SyncThruInputTraySensor(SyncThruSensor): return self.syncthru.input_tray_status().get(self._number, {}) @property - def state(self): + def native_value(self): """Display ready unless there is some error, then display error.""" tray_state = ( self.syncthru.input_tray_status().get(self._number, {}).get("newError") @@ -251,7 +251,7 @@ class SyncThruOutputTraySensor(SyncThruSensor): return self.syncthru.output_tray_status().get(self._number, {}) @property - def state(self): + def native_value(self): """Display ready unless there is some error, then display error.""" tray_state = ( self.syncthru.output_tray_status().get(self._number, {}).get("status") diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index 56e7c54203d..a5b645200db 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -4,7 +4,9 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "invalid_url": "\u00c9rv\u00e9nytelen URL" + "invalid_url": "\u00c9rv\u00e9nytelen URL", + "syncthru_not_supported": "Az eszk\u00f6z nem t\u00e1mogatja a SyncThru-t", + "unknown_state": "A nyomtat\u00f3 \u00e1llapota ismeretlen, ellen\u0151rizze az URL-t \u00e9s a h\u00e1l\u00f3zati kapcsolatot" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/syncthru/translations/zh-Hans.json b/homeassistant/components/syncthru/translations/zh-Hans.json new file mode 100644 index 00000000000..c50e250aee9 --- /dev/null +++ b/homeassistant/components/syncthru/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_state": "\u6253\u5370\u673a\u72b6\u6001\u672a\u77e5\uff0c\u8bf7\u9a8c\u8bc1 URL \u548c\u7f51\u7edc\u662f\u5426\u8fde\u63a5\u6b63\u5e38" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a9ca7b4c48d..0bc88b683b7 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -686,10 +686,10 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): """Initialize the Synology DSM disk or volume entity.""" super().__init__(api, entity_type, entity_info, coordinator) self._device_id = device_id - self._device_name = None - self._device_manufacturer = None - self._device_model = None - self._device_firmware = None + self._device_name: str | None = None + self._device_manufacturer: str | None = None + self._device_model: str | None = None + self._device_firmware: str | None = None self._device_type = None if "volume" in entity_type: @@ -730,8 +730,8 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): (DOMAIN, f"{self._api.information.serial}_{self._device_id}") }, "name": f"Synology NAS ({self._device_name} - {self._device_type})", - "manufacturer": self._device_manufacturer, # type: ignore[typeddict-item] - "model": self._device_model, # type: ignore[typeddict-item] - "sw_version": self._device_firmware, # type: ignore[typeddict-item] + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, "via_device": (DOMAIN, self._api.information.serial), } diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 8341b8b121a..d609a434ae2 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -123,7 +123,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" _LOGGER.debug( "SynoDSMCamera.camera_image(%s)", diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index fdbbb5678c2..633c264f3c8 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -10,7 +10,10 @@ from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_UPDATE, +) from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -81,8 +84,8 @@ UPGRADE_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUpgrade.API_KEY}:update_available": { ATTR_NAME: "Update available", ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_ICON: "mdi:update", - ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_UPDATE, ENTITY_ENABLE: True, ATTR_STATE_CLASS: None, }, 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/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 5942ce4a5b1..1ddc79c0afc 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -90,7 +90,7 @@ class SynoDSMSensor(SynologyDSMBaseEntity): """Mixin for sensor specific attributes.""" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self.entity_type in TEMP_SENSORS_KEYS: return self.hass.config.units.temperature_unit @@ -101,7 +101,7 @@ class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity): """Representation a Synology Utilisation sensor.""" @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.utilisation, self.entity_type) if callable(attr): @@ -133,7 +133,7 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity) """Representation a Synology Storage sensor.""" @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: @@ -166,7 +166,7 @@ class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity): self._last_boot: str | None = None @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.information, self.entity_type) if attr is None: diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index f76ce7ab27a..7b86c248110 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -29,6 +29,10 @@ "description": "\u00bfQuieres configurar {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "description": "Raz\u00f3n: {details}", + "title": "Synology DSM Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 7ac507f1efa..01f02e6156d 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "missing_data": "Hi\u00e1nyz\u00f3 adatok: pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb vagy m\u00e1s konfigur\u00e1ci\u00f3val", + "otp_failed": "A k\u00e9tl\u00e9pcs\u0151s azonos\u00edt\u00e1s sikertelen, pr\u00f3b\u00e1lkozzon \u00faj jelsz\u00f3val", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", @@ -13,7 +16,8 @@ "2sa": { "data": { "otp_code": "K\u00f3d" - } + }, + "title": "Synology DSM: k\u00e9tl\u00e9pcs\u0151s azonos\u00edt\u00e1s" }, "link": { "data": { @@ -23,8 +27,17 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Indokl\u00e1s: {details}", + "title": "Synology DSM Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "Hoszt", @@ -42,6 +55,7 @@ "step": { "init": { "data": { + "scan_interval": "Percek a vizsg\u00e1latok k\u00f6z\u00f6tt", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)" } } diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index c8bb60bcb3e..d1e2d084f0d 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -29,6 +30,14 @@ "description": "Vil du konfigurere {name} ({host})?", "title": "" }, + "reauth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "\u00c5rsak: {details}", + "title": "Synology DSM Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hans.json b/homeassistant/components/synology_dsm/translations/zh-Hans.json index b4edf8039a6..862f526c38d 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hans.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "reauth_successful": "\u91cd\u9a8c\u8bc1" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", @@ -28,13 +29,20 @@ "description": "\u60a8\u60f3\u8981\u914d\u7f6e {name} ({host}) \u5417\uff1f", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, "user": { "data": { "host": "\u4e3b\u673a", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", - "username": "\u7528\u6237\u540d" + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" }, "title": "Synology DSM" } diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index ed3c569f10f..acfcc54f05c 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -92,7 +92,7 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -113,7 +113,7 @@ class SystemBridgeBatterySensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.battery.percent @@ -135,7 +135,7 @@ class SystemBridgeBatteryTimeRemainingSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data if bridge.battery.timeRemaining is None: @@ -159,7 +159,7 @@ class SystemBridgeCpuSpeedSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.currentSpeed.avg @@ -181,7 +181,7 @@ class SystemBridgeCpuTemperatureSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.temperature.main @@ -203,7 +203,7 @@ class SystemBridgeCpuVoltageSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.cpu.voltage @@ -229,7 +229,7 @@ class SystemBridgeFilesystemSensor(SystemBridgeSensor): self._fs_key = key @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -268,7 +268,7 @@ class SystemBridgeMemoryFreeSensor(SystemBridgeSensor): ) @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -294,7 +294,7 @@ class SystemBridgeMemoryUsedSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -320,7 +320,7 @@ class SystemBridgeMemoryUsedPercentageSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -346,7 +346,7 @@ class SystemBridgeKernelSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.os.kernel @@ -368,7 +368,7 @@ class SystemBridgeOsSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return f"{bridge.os.distro} {bridge.os.release}" @@ -390,7 +390,7 @@ class SystemBridgeProcessesLoadSensor(SystemBridgeSensor): ) @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -431,7 +431,7 @@ class SystemBridgeBiosVersionSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.system.bios.version diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c8200e0e10a..651961c72ac 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -43,7 +43,7 @@ def async_register_info( SystemHealthRegistration(hass, domain).async_register_info(info_callback) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the System Health component.""" hass.components.websocket_api.async_register_command(handle_info) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index bc3e922a923..687e9e8e521 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -331,12 +331,12 @@ class SystemMonitorSensor(SensorEntity): return self.sensor_type[SENSOR_TYPE_ICON] # type: ignore[no-any-return] @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" return self.data.state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.sensor_type[SENSOR_TYPE_UOM] # type: ignore[no-any-return] diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 758756e8127..8cf0ed260e8 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.10.0"], - "codeowners": ["@michaelarnauts", "@bdraco", "@noltari"], + "codeowners": ["@michaelarnauts", "@noltari"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index e1219b5620b..537e094bfd2 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -127,7 +127,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): return f"{self._tado.home_name} {self.home_variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -137,7 +137,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.home_variable in ["temperature", "outdoor temperature"]: return TEMP_CELSIUS @@ -232,7 +232,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): return f"{self.zone_name} {self.zone_variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -242,7 +242,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.zone_variable == "temperature": return self.hass.config.units.temperature_unit diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json index fd8db27da5e..dfde73ce428 100644 --- a/homeassistant/components/tado/translations/hu.json +++ b/homeassistant/components/tado/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_homes": "Ehhez a tado-fi\u00f3khoz nincsenek otthonok.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -13,7 +14,19 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon Tado-fi\u00f3kj\u00e1hoz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "A tartal\u00e9k m\u00f3d enged\u00e9lyez\u00e9se." + }, + "description": "A tartal\u00e9k m\u00f3d intelligens \u00fctemez\u00e9sre v\u00e1lt a k\u00f6vetkez\u0151 \u00fctemez\u00e9s kapcsol\u00f3n\u00e1l, miut\u00e1n manu\u00e1lisan be\u00e1ll\u00edtotta a z\u00f3n\u00e1t.", + "title": "\u00c1ll\u00edtsa be a Tado-t." } } } diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 47e6d300414..35a51b03805 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -35,12 +35,12 @@ class TahomaSensor(TahomaDevice, SensorEntity): super().__init__(tahoma_device, controller) @property - def state(self): + def native_value(self): """Return the name of the sensor.""" return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.type == "io:TemperatureIOSystemSensor": return TEMP_CELSIUS diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 379819cf65e..93794ce0c50 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -81,7 +81,7 @@ class TankUtilitySensor(SensorEntity): return self._device @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -91,7 +91,7 @@ class TankUtilitySensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the device.""" return self._unit_of_measurement diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 5c1898e02a9..166e1da7060 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -110,12 +110,12 @@ class FuelPriceSensor(CoordinatorEntity, SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return CURRENCY_EURO @property - def state(self): + def native_value(self): """Return the state of the device.""" # key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions return self.coordinator.data[self._station_id].get(self._fuel_type) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index b756d656921..39ee97d1648 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from hatasmota import const as hc, sensor as tasmota_sensor, status_sensor @@ -10,7 +9,11 @@ from hatasmota.entity import TasmotaEntity as HATasmotaEntity from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -49,14 +52,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import dt as dt_util from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -_LOGGER = logging.getLogger(__name__) - DEVICE_CLASS = "device_class" STATE_CLASS = "state_class" ICON = "icon" @@ -121,7 +121,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, hc.SENSOR_TOTAL: { DEVICE_CLASS: DEVICE_CLASS_ENERGY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, @@ -188,7 +188,6 @@ async def async_setup_entry( class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" - _attr_last_reset = None _tasmota_entity: tasmota_sensor.TasmotaSensor def __init__(self, **kwds: Any) -> None: @@ -212,17 +211,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._state_timestamp = state else: self._state = state - if "last_reset" in kwargs: - try: - last_reset_dt = dt_util.parse_datetime(kwargs["last_reset"]) - last_reset = dt_util.as_utc(last_reset_dt) if last_reset_dt else None - if last_reset is None: - raise ValueError - self._attr_last_reset = last_reset - except ValueError: - _LOGGER.warning( - "Invalid last_reset timestamp '%s'", kwargs["last_reset"] - ) self.async_write_ha_state() @property @@ -258,7 +246,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return class_or_icon.get(ICON) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: return self._state_timestamp.isoformat() @@ -270,6 +258,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return True @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return SENSOR_UNIT_MAP.get(self._tasmota_entity.unit, self._tasmota_entity.unit) diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index cb2e38ebd6d..d413e477397 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -2,7 +2,7 @@ "domain": "tautulli", "name": "Tautulli", "documentation": "https://www.home-assistant.io/integrations/tautulli", - "requirements": ["pytautulli==0.5.0"], + "requirements": ["pytautulli==21.8.1"], "codeowners": ["@ludeeus"], "iot_class": "local_polling" } diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index c50efb00ed7..16b58b206aa 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -1,7 +1,7 @@ """A platform which allows you to get information from Tautulli.""" from datetime import timedelta -from pytautulli import Tautulli +from pytautulli import PyTautulli import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -60,10 +60,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass, verify_ssl) tautulli = TautulliData( - Tautulli(host, port, api_key, hass.loop, session, use_ssl, path) + PyTautulli( + api_token=api_key, + hostname=host, + session=session, + verify_ssl=verify_ssl, + port=port, + ssl=use_ssl, + base_api_path=path, + ) ) - if not await tautulli.test_connection(): + await tautulli.async_update() + if not tautulli.activity or not tautulli.home_stats or not tautulli.users: raise PlatformNotReady sensor = [TautulliSensor(tautulli, name, monitored_conditions, user)] @@ -88,25 +97,52 @@ class TautulliSensor(SensorEntity): async def async_update(self): """Get the latest data from the Tautulli API.""" await self.tautulli.async_update() - self.home = self.tautulli.api.home_data - self.sessions = self.tautulli.api.session_data - self._attributes["Top Movie"] = self.home.get("movie") - self._attributes["Top TV Show"] = self.home.get("tv") - self._attributes["Top User"] = self.home.get("user") - for key in self.sessions: - if "sessions" not in key: - self._attributes[key] = self.sessions[key] - for user in self.tautulli.api.users: - if self.usernames is None or user in self.usernames: - userdata = self.tautulli.api.user_data - self._attributes[user] = {} - self._attributes[user]["Activity"] = userdata[user]["Activity"] - if self.monitored_conditions: - for key in self.monitored_conditions: - try: - self._attributes[user][key] = userdata[user][key] - except (KeyError, TypeError): - self._attributes[user][key] = "" + if ( + not self.tautulli.activity + or not self.tautulli.home_stats + or not self.tautulli.users + ): + return + + self._attributes = { + "stream_count": self.tautulli.activity.stream_count, + "stream_count_direct_play": self.tautulli.activity.stream_count_direct_play, + "stream_count_direct_stream": self.tautulli.activity.stream_count_direct_stream, + "stream_count_transcode": self.tautulli.activity.stream_count_transcode, + "total_bandwidth": self.tautulli.activity.total_bandwidth, + "lan_bandwidth": self.tautulli.activity.lan_bandwidth, + "wan_bandwidth": self.tautulli.activity.wan_bandwidth, + } + + for stat in self.tautulli.home_stats: + if stat.stat_id == "top_movies": + self._attributes["Top Movie"] = ( + stat.rows[0].title if stat.rows else None + ) + elif stat.stat_id == "top_tv": + self._attributes["Top TV Show"] = ( + stat.rows[0].title if stat.rows else None + ) + elif stat.stat_id == "top_users": + self._attributes["Top User"] = stat.rows[0].user if stat.rows else None + + for user in self.tautulli.users: + if ( + self.usernames + and user.username not in self.usernames + or user.username == "Local" + ): + continue + self._attributes.setdefault(user.username, {})["Activity"] = None + + for session in self.tautulli.activity.sessions: + if not self._attributes.get(session.username): + continue + + self._attributes[session.username]["Activity"] = session.state + if self.monitored_conditions: + for key in self.monitored_conditions: + self._attributes[session.username][key] = getattr(session, key) @property def name(self): @@ -114,9 +150,11 @@ class TautulliSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" - return self.sessions.get("stream_count") + if not self.tautulli.activity: + return 0 + return self.tautulli.activity.stream_count @property def icon(self): @@ -124,7 +162,7 @@ class TautulliSensor(SensorEntity): return "mdi:plex" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return "Watching" @@ -140,14 +178,13 @@ class TautulliData: def __init__(self, api): """Initialize the data object.""" self.api = api + self.activity = None + self.home_stats = None + self.users = None @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Tautulli.""" - await self.api.get_data() - - async def test_connection(self): - """Test connection to Tautulli.""" - await self.api.test_connection() - connection_status = self.api.connection - return connection_status + self.activity = await self.api.async_get_activity() + self.home_stats = await self.api.async_get_home_stats() + self.users = await self.api.async_get_users() diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index d282974fd4c..4db511e1f57 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -31,11 +31,11 @@ class TcpSensor(TcpEntity, SensorEntity): """Implementation of a TCP socket based sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the device.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._config[CONF_UNIT_OF_MEASUREMENT] diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 6732014c747..a7162ee9c63 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -79,12 +79,12 @@ class Ted5000Sensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the resources.""" with suppress(KeyError): return self._gateway.data[self._mtu][self._unit] diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index a86b487afd2..35fc6809523 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -111,7 +111,7 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): return "{} {}".format(super().name, self.quantity_name or "").strip() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if not self.available: return None @@ -129,7 +129,7 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): return SENSOR_TYPES[self._type][0] if self._type in SENSOR_TYPES else None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_TYPES[self._type][1] if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 599c19388d6..74548f94d1b 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -154,12 +154,12 @@ class TellstickSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 7d447d3f9ea..ffb5660109c 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -77,12 +77,12 @@ class TemperSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self.temp_unit diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index e9afdbd8cb4..4214323c8ee 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -255,7 +255,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): except template.TemplateError: pass - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._template = state_template self._attr_device_class = device_class self._attr_state_class = state_class @@ -264,7 +264,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute( - "_attr_state", self._template, None, self._update_state + "_attr_native_value", self._template, None, self._update_state ) if self._friendly_name_template and not self._friendly_name_template.is_static: self.add_template_attribute("_attr_name", self._friendly_name_template) @@ -274,7 +274,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): @callback def _update_state(self, result): super()._update_state(result) - self._attr_state = None if isinstance(result, TemplateError) else result + self._attr_native_value = None if isinstance(result, TemplateError) else result class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -284,7 +284,7 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): extra_template_keys = (CONF_STATE,) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) 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() diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index ad585082b48..60e3e19047d 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -38,7 +38,7 @@ class TeslaSensor(TeslaDevice, SensorEntity): self._unique_id = f"{super().unique_id}_{self.type}" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if self.tesla_device.type == "temperature sensor": if self.type == "outside": @@ -57,7 +57,7 @@ class TeslaSensor(TeslaDevice, SensorEntity): return self.tesla_device.get_value() @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit_of_measurement of the device.""" units = self.tesla_device.measurement if units == "F": diff --git a/homeassistant/components/tesla/translations/es.json b/homeassistant/components/tesla/translations/es.json index 54fbfd1a21d..8211e806741 100644 --- a/homeassistant/components/tesla/translations/es.json +++ b/homeassistant/components/tesla/translations/es.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "C\u00f3digo MFA (opcional)", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, diff --git a/homeassistant/components/tesla/translations/hu.json b/homeassistant/components/tesla/translations/hu.json index a4622ce7efa..75a93566df5 100644 --- a/homeassistant/components/tesla/translations/hu.json +++ b/homeassistant/components/tesla/translations/hu.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA k\u00f3d (opcion\u00e1lis)", "password": "Jelsz\u00f3", "username": "E-mail" }, @@ -24,6 +25,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Az aut\u00f3k \u00e9bred\u00e9sre k\u00e9nyszer\u00edt\u00e9se ind\u00edt\u00e1skor", "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" } } diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json index ce706640636..11e49486107 100644 --- a/homeassistant/components/tesla/translations/no.json +++ b/homeassistant/components/tesla/translations/no.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA -kode (valgfritt)", "password": "Passord", "username": "E-post" }, diff --git a/homeassistant/components/tesla/translations/zh-Hans.json b/homeassistant/components/tesla/translations/zh-Hans.json new file mode 100644 index 00000000000..35635ce3be3 --- /dev/null +++ b/homeassistant/components/tesla/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mfa": "MFA \u4ee3\u7801\uff08\u53ef\u9009\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 4b14c9a9305..2e4ef6e56ec 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -120,7 +120,7 @@ class ThermoworksSmokeSensor(SensorEntity): return self._unique_id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -130,7 +130,7 @@ class ThermoworksSmokeSensor(SensorEntity): return self._attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 2e139eae63d..089d1eda2ee 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -76,7 +76,7 @@ class TtnDataSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the entity.""" if self._ttn_data_storage.data is not None: try: @@ -86,7 +86,7 @@ class TtnDataSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index fa1dfd5988c..e7530636169 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -95,12 +95,12 @@ class ThinkingCleanerSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement 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 diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index b5012cdc41d..d376bf0a7d5 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -2,9 +2,7 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass from datetime import timedelta -from enum import Enum import logging from random import randrange @@ -19,6 +17,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -26,17 +25,15 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + EVENT_HOMEASSISTANT_STOP, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util @@ -51,171 +48,150 @@ PARALLEL_UPDATES = 0 SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" -class ResetType(Enum): - """Data reset type.""" - - HOURLY = "hourly" - DAILY = "daily" - NEVER = "never" - - -@dataclass -class TibberSensorEntityDescription(SensorEntityDescription): - """Describes Tibber sensor entity.""" - - reset_type: ResetType | None = None - - -RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { - "averagePower": TibberSensorEntityDescription( +RT_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="averagePower", name="average power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), - "power": TibberSensorEntityDescription( + SensorEntityDescription( key="power", name="power", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), - "powerProduction": TibberSensorEntityDescription( + SensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), - "minPower": TibberSensorEntityDescription( + SensorEntityDescription( key="minPower", name="min power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), - "maxPower": TibberSensorEntityDescription( + SensorEntityDescription( key="maxPower", name="max power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), - "accumulatedConsumption": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedConsumption", name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "accumulatedConsumptionLastHour": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.HOURLY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "accumulatedProduction": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedProduction", name="accumulated production", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "accumulatedProductionLastHour": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedProductionLastHour", name="accumulated production current hour", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.HOURLY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "lastMeterConsumption": TibberSensorEntityDescription( + SensorEntityDescription( key="lastMeterConsumption", name="last meter consumption", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "lastMeterProduction": TibberSensorEntityDescription( + SensorEntityDescription( key="lastMeterProduction", name="last meter production", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - "voltagePhase1": TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase1", name="voltage phase1", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase2": TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase2", name="voltage phase2", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase3": TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase3", name="voltage phase3", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL1": TibberSensorEntityDescription( + SensorEntityDescription( key="currentL1", name="current L1", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL2": TibberSensorEntityDescription( + SensorEntityDescription( key="currentL2", name="current L2", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL3": TibberSensorEntityDescription( + SensorEntityDescription( key="currentL3", name="current L3", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "signalStrength": TibberSensorEntityDescription( + SensorEntityDescription( key="signalStrength", name="signal strength", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), - "accumulatedReward": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedReward", name="accumulated reward", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, ), - "accumulatedCost": TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedCost", name="accumulated cost", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, ), - "powerFactor": TibberSensorEntityDescription( + SensorEntityDescription( key="powerFactor", name="power factor", device_class=DEVICE_CLASS_POWER_FACTOR, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), -} +) async def async_setup_entry(hass, entry, async_add_entities): @@ -241,7 +217,9 @@ async def async_setup_entry(hass, entry, async_add_entities): entities.append(TibberSensorElPrice(home)) if home.has_real_time_consumption: await home.rt_subscribe( - TibberRtDataHandler(async_add_entities, home, hass).async_callback + TibberRtDataCoordinator( + async_add_entities, home, hass + ).async_set_updated_data ) # migrate @@ -271,27 +249,23 @@ async def async_setup_entry(hass, entry, async_add_entities): class TibberSensor(SensorEntity): """Representation of a generic Tibber sensor.""" - def __init__(self, tibber_home): + def __init__(self, *args, tibber_home, **kwargs): """Initialize the sensor.""" + super().__init__(*args, **kwargs) self._tibber_home = tibber_home self._home_name = tibber_home.info["viewer"]["home"]["appNickname"] - self._device_name = None if self._home_name is None: self._home_name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" ) + self._device_name = None self._model = None - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self._tibber_home.home_id - @property def device_info(self): """Return the device_info of the device.""" device_info = { - "identifiers": {(TIBBER_DOMAIN, self.device_id)}, + "identifiers": {(TIBBER_DOMAIN, self._tibber_home.home_id)}, "name": self._device_name, "manufacturer": MANUFACTURER, } @@ -305,7 +279,7 @@ class TibberSensorElPrice(TibberSensor): def __init__(self, tibber_home): """Initialize the sensor.""" - super().__init__(tibber_home) + super().__init__(tibber_home=tibber_home) self._last_updated = None self._spread_load_constant = randrange(5000) @@ -350,13 +324,13 @@ class TibberSensorElPrice(TibberSensor): return res = self._tibber_home.current_price_data() - self._attr_state, price_level, self._last_updated = res + self._attr_native_value, price_level, self._last_updated = res self._attr_extra_state_attributes["price_level"] = price_level attrs = self._tibber_home.current_attributes() self._attr_extra_state_attributes.update(attrs) - self._attr_available = self._attr_state is not None - self._attr_unit_of_measurement = self._tibber_home.price_unit + self._attr_available = self._attr_native_value is not None + self._attr_native_unit_of_measurement = self._tibber_home.price_unit @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _fetch_data(self): @@ -375,52 +349,28 @@ class TibberSensorElPrice(TibberSensor): ]["estimatedAnnualConsumption"] -class TibberSensorRT(TibberSensor): +class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): """Representation of a Tibber sensor for real time consumption.""" - _attr_should_poll = False - entity_description: TibberSensorEntityDescription - def __init__( self, tibber_home, - description: TibberSensorEntityDescription, + description: SensorEntityDescription, initial_state, + coordinator: TibberRtDataCoordinator, ): """Initialize the sensor.""" - super().__init__(tibber_home) + super().__init__(coordinator=coordinator, tibber_home=tibber_home) self.entity_description = description self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" self._attr_name = f"{description.name} {self._home_name}" - self._attr_state = initial_state + self._attr_native_value = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" - if description.name in ("accumulated cost", "accumulated reward"): - self._attr_unit_of_measurement = tibber_home.currency - if description.reset_type == ResetType.NEVER: - self._attr_last_reset = dt_util.utc_from_timestamp(0) - elif description.reset_type == ResetType.DAILY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif description.reset_type == ResetType.HOURLY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(minute=0, second=0, microsecond=0) - ) - else: - self._attr_last_reset = None - - async def async_added_to_hass(self): - """Start listen for real time data.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_UPDATE_ENTITY.format(self.unique_id), - self._set_state, - ) - ) + if description.key in ("accumulatedCost", "accumulatedReward"): + self._attr_native_unit_of_measurement = tibber_home.currency @property def available(self): @@ -428,27 +378,19 @@ class TibberSensorRT(TibberSensor): return self._tibber_home.rt_subscription_running @callback - def _set_state(self, state, timestamp): - """Set sensor state.""" - if ( - state < self._attr_state - and self.entity_description.reset_type == ResetType.DAILY - ): - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(hour=0, minute=0, second=0, microsecond=0) - ) - if ( - state < self._attr_state - and self.entity_description.reset_type == ResetType.HOURLY - ): - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(minute=0, second=0, microsecond=0) - ) - self._attr_state = state + def _handle_coordinator_update(self) -> None: + if not (live_measurement := self.coordinator.get_live_measurement()): # type: ignore[attr-defined] + return + state = live_measurement.get(self.entity_description.key) + if state is None: + return + if self.entity_description.key == "powerFactor": + state *= 100.0 + self._attr_native_value = state self.async_write_ha_state() -class TibberRtDataHandler: +class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): """Handle Tibber realtime data.""" def __init__(self, async_add_entities, tibber_home, hass): @@ -456,42 +398,53 @@ class TibberRtDataHandler: self._async_add_entities = async_add_entities self._tibber_home = tibber_home self.hass = hass - self._entities = {} + self._added_sensors = set() + super().__init__( + hass, + _LOGGER, + name=tibber_home.info["viewer"]["home"]["address"].get( + "address1", "Tibber" + ), + ) - async def async_callback(self, payload): - """Handle received data.""" - errors = payload.get("errors") - if errors: - _LOGGER.error(errors[0]) - return - data = payload.get("data") - if data is None: - return - live_measurement = data.get("liveMeasurement") - if live_measurement is None: + self._async_remove_device_updates_handler = self.async_add_listener( + self._add_sensors + ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + + @callback + def _handle_ha_stop(self, _event) -> None: + """Handle Home Assistant stopping.""" + self._async_remove_device_updates_handler() + + @callback + def _add_sensors(self): + """Add sensor.""" + if not (live_measurement := self.get_live_measurement()): return - timestamp = dt_util.parse_datetime(live_measurement.pop("timestamp")) new_entities = [] - for sensor_type, state in live_measurement.items(): - if state is None or sensor_type not in RT_SENSOR_MAP: + for sensor_description in RT_SENSORS: + if sensor_description.key in self._added_sensors: continue - if sensor_type == "powerFactor": - state *= 100.0 - if sensor_type in self._entities: - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_ENTITY.format(self._entities[sensor_type]), - state, - timestamp, - ) - else: - entity = TibberSensorRT( - self._tibber_home, - RT_SENSOR_MAP[sensor_type], - state, - ) - new_entities.append(entity) - self._entities[sensor_type] = entity.unique_id + state = live_measurement.get(sensor_description.key) + if state is None: + continue + entity = TibberSensorRT( + self._tibber_home, + sensor_description, + state, + self, + ) + new_entities.append(entity) + self._added_sensors.add(sensor_description.key) if new_entities: self._async_add_entities(new_entities) + + def get_live_measurement(self): + """Get live measurement data.""" + errors = self.data.get("errors") + if errors: + _LOGGER.error(errors[0]) + return None + return self.data.get("data", {}).get("liveMeasurement") diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 08195e6dd3d..58582b3b139 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -65,7 +65,7 @@ class TimeDateSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index 88471a86c27..d777fec38b6 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -85,7 +85,7 @@ class TMBSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit @@ -95,7 +95,7 @@ class TMBSensor(SensorEntity): return f"{self._stop}_{self._line}" @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 85e975e94ff..d0b680375f9 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -30,7 +30,7 @@ new_task: min: 1 max: 4 due_date_string: - name: Dure date string + name: Due date string description: The day this task is due, in natural language. example: Tomorrow selector: diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index 45713dd8f77..631018f55cd 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -82,12 +82,12 @@ class VL53L1XSensor(SensorEntity): return self._name @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 4af57e03412..678b3400b88 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -6,24 +6,25 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, ) from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_GAS, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, + VOLUME_CUBIC_METERS, ) -from homeassistant.util import dt as dt_util DOMAIN = "toon" @@ -38,7 +39,6 @@ DEFAULT_MIN_TEMP = 6.0 CURRENCY_EUR = "EUR" VOLUME_CM3 = "CM3" -VOLUME_M3 = "M3" VOLUME_LHOUR = "L/H" VOLUME_LMIN = "L/MIN" @@ -125,16 +125,16 @@ SENSOR_ENTITIES = { ATTR_NAME: "Average Daily Gas Usage", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_average", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_ICON: "mdi:gas-cylinder", + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_DEFAULT_ENABLED: False, }, "gas_daily_usage": { ATTR_NAME: "Gas Usage Today", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_usage", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_ICON: "mdi:gas-cylinder", + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, }, "gas_daily_cost": { ATTR_NAME: "Gas Cost Today", @@ -147,10 +147,9 @@ SENSOR_ENTITIES = { ATTR_NAME: "Gas Meter", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "meter", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_ICON: "mdi:gas-cylinder", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, ATTR_DEFAULT_ENABLED: False, }, "gas_value": { @@ -196,8 +195,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "power_meter_reading_low": { @@ -206,8 +204,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "power_value": { @@ -224,8 +221,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "solar_meter_reading_low_produced": { @@ -234,8 +230,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "solar_value": { @@ -321,7 +316,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Average Daily Water Usage", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_average", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, }, @@ -329,7 +324,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Water Usage Today", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_usage", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, }, @@ -337,11 +332,10 @@ SENSOR_ENTITIES = { ATTR_NAME: "Water Meter", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "meter", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "water_value": { ATTR_NAME: "Current Water Usage", diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index b16672674af..4522e34943c 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,11 +1,7 @@ """Support for Toon sensors.""" from __future__ import annotations -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - ATTR_STATE_CLASS, - SensorEntity, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -127,10 +123,9 @@ class ToonSensor(ToonEntity, SensorEntity): ATTR_DEFAULT_ENABLED, True ) self._attr_icon = sensor.get(ATTR_ICON) - self._attr_last_reset = sensor.get(ATTR_LAST_RESET) self._attr_name = sensor[ATTR_NAME] self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] + self._attr_native_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) self._attr_unique_id = ( # This unique ID is a bit ugly and contains unneeded information. @@ -139,7 +134,7 @@ class ToonSensor(ToonEntity, SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" section = getattr( self.coordinator.data, SENSOR_ENTITIES[self.key][ATTR_SECTION] diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index 6371bf4c6fd..18f333dccdf 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/translations/hu.json @@ -1,11 +1,24 @@ { "config": { "abort": { + "already_configured": "A kiv\u00e1lasztott meg\u00e1llapod\u00e1s m\u00e1r konfigur\u00e1lva van.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_agreements": "Ennek a fi\u00f3knak nincsenek Toon kijelz\u0151i.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." + }, + "step": { + "agreement": { + "data": { + "agreement": "Meg\u00e1llapod\u00e1s" + }, + "description": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt szerz\u0151d\u00e9sc\u00edmet.", + "title": "V\u00e1lassza ki a meg\u00e1llapod\u00e1st" + }, + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9shez" + } } } } \ No newline at end of file diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8e3053d9bd8..162dd5f437c 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -120,12 +120,12 @@ class TorqueSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index e9e991d81d4..319611fd2b1 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -25,7 +25,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Total Connect" } } } diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 552e5666db8..aad934b2600 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -1,7 +1,7 @@ """Component to embed TP-Link smart home devices.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging import time from typing import Any @@ -11,7 +11,6 @@ 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 ( @@ -28,7 +27,6 @@ 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 .common import SmartDevices, async_discover_devices, get_static_devices from .const import ( @@ -156,7 +154,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( @@ -170,7 +168,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", @@ -181,7 +179,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: @@ -217,16 +215,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 @@ -239,9 +241,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 @@ -251,19 +251,17 @@ 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), 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 @@ -276,3 +274,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) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 697641915f7..b38fa763ee9 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -6,8 +6,8 @@ from typing import Any, Final from pyHS100 import SmartPlug from homeassistant.components.sensor import ( - ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -62,14 +62,14 @@ ENERGY_SENSORS: Final[list[SensorEntityDescription]] = [ key=ATTR_TOTAL_ENERGY_KWH, unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, 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, + state_class=STATE_CLASS_TOTAL_INCREASING, name="Today's Consumption", ), SensorEntityDescription( @@ -127,9 +127,6 @@ class SmartPlugSensor(CoordinatorEntity, SensorEntity): 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]: @@ -137,7 +134,7 @@ class SmartPlugSensor(CoordinatorEntity, SensorEntity): return self.coordinator.data @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the sensors state.""" return self.data[CONF_EMETER_PARAMS][self.entity_description.key] diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 5ad5879f31b..16cd9ba94e5 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -74,6 +74,26 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL +EVENTS = [ + EVENT_DEVICE_MOVING, + EVENT_COMMAND_RESULT, + EVENT_DEVICE_FUEL_DROP, + EVENT_GEOFENCE_ENTER, + EVENT_DEVICE_OFFLINE, + EVENT_DRIVER_CHANGED, + EVENT_GEOFENCE_EXIT, + EVENT_DEVICE_OVERSPEED, + EVENT_DEVICE_ONLINE, + EVENT_DEVICE_STOPPED, + EVENT_MAINTENANCE, + EVENT_ALARM, + EVENT_TEXT_MESSAGE, + EVENT_DEVICE_UNKNOWN, + EVENT_IGNITION_OFF, + EVENT_IGNITION_ON, + EVENT_ALL_EVENTS, +] + PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, @@ -91,27 +111,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_EVENT, default=[]): vol.All( cv.ensure_list, - [ - vol.Any( - EVENT_DEVICE_MOVING, - EVENT_COMMAND_RESULT, - EVENT_DEVICE_FUEL_DROP, - EVENT_GEOFENCE_ENTER, - EVENT_DEVICE_OFFLINE, - EVENT_DRIVER_CHANGED, - EVENT_GEOFENCE_EXIT, - EVENT_DEVICE_OVERSPEED, - EVENT_DEVICE_ONLINE, - EVENT_DEVICE_STOPPED, - EVENT_MAINTENANCE, - EVENT_ALARM, - EVENT_TEXT_MESSAGE, - EVENT_DEVICE_UNKNOWN, - EVENT_IGNITION_OFF, - EVENT_IGNITION_ON, - EVENT_ALL_EVENTS, - ) - ], + [vol.In(EVENTS)], ), } ) @@ -203,6 +203,8 @@ class TraccarScanner: ): """Initialize.""" + if EVENT_ALL_EVENTS in event_types: + event_types = EVENTS self._event_types = {camelcase(evt): evt for evt in event_types} self._custom_attributes = custom_attributes self._scan_interval = scan_interval diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index c4fc027d059..94fc9198921 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -6,6 +6,12 @@ }, "create_entry": { "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + }, + "step": { + "user": { + "description": "Biztosan be\u00e1ll\u00edtja a Traccar szolg\u00e1ltat\u00e1st?", + "title": "A Traccar be\u00e1ll\u00edt\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index cb8eff1c8bb..78ee4c7ed97 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -9,7 +9,7 @@ import aiotractive from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -38,6 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: creds = await client.authenticate() + except aiotractive.exceptions.UnauthorizedError as error: + await client.close() + raise ConfigEntryAuthFailed from error except aiotractive.exceptions.TractiveError as error: await client.close() raise ConfigEntryNotReady from error diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index 70ed9071c7b..4b1fc241110 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -17,7 +17,9 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema({CONF_EMAIL: str, CONF_PASSWORD: str}) +USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} +) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: @@ -47,9 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) + return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) errors = {} @@ -66,7 +66,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, _: dict[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + + errors = {} + + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + existing_entry = await self.async_set_unique_id(info["user_id"]) + if existing_entry: + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_failed_existing") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=USER_DATA_SCHEMA, + errors=errors, ) diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 5d265c489ff..7587fedfc4c 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,7 +6,7 @@ DOMAIN = "tractive" RECONNECT_INTERVAL = timedelta(seconds=10) -TRACKER_HARDWARE_STATUS_UPDATED = "tracker_hardware_status_updated" -TRACKER_POSITION_UPDATED = "tracker_position_updated" +TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" +TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" -SERVER_UNAVAILABLE = "tractive_server_unavailable" +SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 1365faa6419..82e22139f04 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -94,52 +94,52 @@ class TractiveDeviceTracker(TrackerEntity): """Return the battery level of the device.""" return self._battery_level + @callback + def _handle_hardware_status_update(self, event): + self._battery_level = event["battery_level"] + self._attr_available = True + self.async_write_ha_state() + + @callback + def _handle_position_update(self, event): + self._latitude = event["latitude"] + self._longitude = event["longitude"] + self._accuracy = event["accuracy"] + self._attr_available = True + self.async_write_ha_state() + + @callback + def _handle_server_unavailable(self): + self._latitude = None + self._longitude = None + self._accuracy = None + self._battery_level = None + self._attr_available = False + self.async_write_ha_state() + async def async_added_to_hass(self): """Handle entity which will be added.""" - @callback - def handle_hardware_status_update(event): - self._battery_level = event["battery_level"] - self._attr_available = True - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", - handle_hardware_status_update, + self._handle_hardware_status_update, ) ) - @callback - def handle_position_update(event): - self._latitude = event["latitude"] - self._longitude = event["longitude"] - self._accuracy = event["accuracy"] - self._attr_available = True - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{TRACKER_POSITION_UPDATED}-{self._tracker_id}", - handle_position_update, + self._handle_position_update, ) ) - @callback - def handle_server_unavailable(): - self._latitude = None - self._longitude = None - self._accuracy = None - self._battery_level = None - self._attr_available = False - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{SERVER_UNAVAILABLE}-{self._user_id}", - handle_server_unavailable, + self._handle_server_unavailable, ) ) diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 2328c07f905..73ee75a4ac5 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tractive", "requirements": [ - "aiotractive==0.5.1" + "aiotractive==0.5.2" ], "codeowners": [ "@Danielhiversen", diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 510b5697e56..9711eb41489 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -13,7 +13,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again." } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/ca.json b/homeassistant/components/tractive/translations/ca.json new file mode 100644 index 00000000000..0641dd2737b --- /dev/null +++ b/homeassistant/components/tractive/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_failed_existing": "No s'ha pogut actualitzar l'entrada de configuraci\u00f3, elimina la integraci\u00f3 i torna-la a instal\u00b7lar.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/cs.json b/homeassistant/components/tractive/translations/cs.json new file mode 100644 index 00000000000..3ad489e1f5e --- /dev/null +++ b/homeassistant/components/tractive/translations/cs.json @@ -0,0 +1,20 @@ +{ + "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": { + "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/tractive/translations/de.json b/homeassistant/components/tractive/translations/de.json new file mode 100644 index 00000000000..cad80fd36a8 --- /dev/null +++ b/homeassistant/components/tractive/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_failed_existing": "Der Konfigurationseintrag konnte nicht aktualisiert werden. Bitte entferne die Integration und richte sie erneut ein.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/en.json b/homeassistant/components/tractive/translations/en.json index 4abfd682903..dcb3a128ac4 100644 --- a/homeassistant/components/tractive/translations/en.json +++ b/homeassistant/components/tractive/translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", @@ -10,7 +12,7 @@ "step": { "user": { "data": { - "email": "E-Mail", + "email": "Email", "password": "Password" } } diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json new file mode 100644 index 00000000000..11aa4f1aa9c --- /dev/null +++ b/homeassistant/components/tractive/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El sistema ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea", + "unknown": "Error desconocido" + }, + "step": { + "user": { + "data": { + "email": "Correo-e", + "password": "Clave" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/et.json b/homeassistant/components/tractive/translations/et.json new file mode 100644 index 00000000000..67adf622ebe --- /dev/null +++ b/homeassistant/components/tractive/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_failed_existing": "Seadekirjet ei \u00f5nnestunud uuendada, eemalda sidumine ja seadista see uuesti.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/fr.json b/homeassistant/components/tractive/translations/fr.json new file mode 100644 index 00000000000..1d3c15c13d5 --- /dev/null +++ b/homeassistant/components/tractive/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositif d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Adresse mail", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/he.json b/homeassistant/components/tractive/translations/he.json new file mode 100644 index 00000000000..1cccac175a0 --- /dev/null +++ b/homeassistant/components/tractive/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "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": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json new file mode 100644 index 00000000000..d0f75a28ed0 --- /dev/null +++ b/homeassistant/components/tractive/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "Ismeretlen hiba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/it.json b/homeassistant/components/tractive/translations/it.json new file mode 100644 index 00000000000..44cdc2df3d7 --- /dev/null +++ b/homeassistant/components/tractive/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_failed_existing": "Impossibile aggiornare la voce di configurazione, rimuovere l'integrazione e configurarla di nuovo.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/nl.json b/homeassistant/components/tractive/translations/nl.json new file mode 100644 index 00000000000..b0e1f17cdc3 --- /dev/null +++ b/homeassistant/components/tractive/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_failed_existing": "Kon het configuratie-item niet bijwerken, verwijder de integratie en stel deze opnieuw in.", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/no.json b/homeassistant/components/tractive/translations/no.json new file mode 100644 index 00000000000..a768b453848 --- /dev/null +++ b/homeassistant/components/tractive/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/pl.json b/homeassistant/components/tractive/translations/pl.json new file mode 100644 index 00000000000..da4e71dc1b7 --- /dev/null +++ b/homeassistant/components/tractive/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/pt.json b/homeassistant/components/tractive/translations/pt.json new file mode 100644 index 00000000000..7430480cc09 --- /dev/null +++ b/homeassistant/components/tractive/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/ru.json b/homeassistant/components/tractive/translations/ru.json new file mode 100644 index 00000000000..89042b79b5e --- /dev/null +++ b/homeassistant/components/tractive/translations/ru.json @@ -0,0 +1,21 @@ +{ + "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_failed_existing": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430.", + "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": { + "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": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/zh-Hans.json b/homeassistant/components/tractive/translations/zh-Hans.json new file mode 100644 index 00000000000..5d8e6c66984 --- /dev/null +++ b/homeassistant/components/tractive/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u5b58\u5728\u914d\u7f6e\u6587\u6863" + }, + "error": { + "invalid_auth": "\u8ba4\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "email": "\u7535\u5b50\u90ae\u7bb1", + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/zh-Hant.json b/homeassistant/components/tractive/translations/zh-Hant.json new file mode 100644 index 00000000000..8c9ec055f63 --- /dev/null +++ b/homeassistant/components/tractive/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_failed_existing": "\u7121\u6cd5\u66f4\u65b0\u8a2d\u5b9a\u5be6\u9ad4\uff0c\u8acb\u79fb\u9664\u6574\u5408\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index cf39d3d6c05..2c113b63727 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,6 +1,9 @@ """Support for IKEA Tradfri.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory @@ -14,7 +17,6 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util.json import load_json from .const import ( ATTR_TRADFRI_GATEWAY, @@ -26,7 +28,6 @@ from .const import ( CONF_IDENTITY, CONF_IMPORT_GROUPS, CONF_KEY, - CONFIG_FILE, DEFAULT_ALLOW_TRADFRI_GROUPS, DEVICES, DOMAIN, @@ -55,7 +56,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tradfri component.""" conf = config.get(DOMAIN) @@ -66,27 +67,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) ] - legacy_hosts = await hass.async_add_executor_job( - load_json, hass.config.path(CONFIG_FILE) - ) - - for host, info in legacy_hosts.items(): - if host in configured_hosts: - continue - - info[CONF_HOST] = host - info[CONF_IMPORT_GROUPS] = conf[CONF_ALLOW_TRADFRI_GROUPS] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=info - ) - ) - host = conf.get(CONF_HOST) import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] - if host is None or host in configured_hosts or host in legacy_hosts: + if host is None or host in configured_hosts: return True hass.async_create_task( @@ -103,7 +87,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Create a gateway.""" # host, identity, key, allow_tradfri_groups - tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + tradfri_data: dict[str, Any] = {} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data listeners = tradfri_data[LISTENERS] = [] factory = await APIFactory.init( diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index f7c2bf6cbe5..1f382548263 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -15,7 +15,6 @@ CONF_IDENTITY = "identity" CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" CONF_KEY = "key" -CONFIG_FILE = ".tradfri_psk.conf" DEFAULT_ALLOW_TRADFRI_GROUPS = False DOMAIN = "tradfri" KEY_API = "tradfri_api" diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 3e13cdc015a..7ffad04074d 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,6 +7,6 @@ "homekit": { "models": ["TRADFRI"] }, - "codeowners": [], + "codeowners": ["@janiversen"], "iot_class": "local_polling" } diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 1f028849d32..f7f68b666ba 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -30,7 +30,7 @@ class TradfriSensor(TradfriBaseDevice, SensorEntity): """The platform class required by Home Assistant.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device, api, gateway_id): """Initialize the device.""" @@ -38,6 +38,6 @@ class TradfriSensor(TradfriBaseDevice, SensorEntity): self._unique_id = f"{gateway_id}-{device.id}" @property - def state(self): + def native_value(self): """Return the current state of the device.""" return self._device.device_info.battery_level diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 5e541045266..cd5cdf29521 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -189,7 +189,7 @@ class TrainSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the departure state.""" state = self._state if state is not None: diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 1ae090ea231..1435da6a988 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -185,12 +185,12 @@ class TrafikverketWeatherStation(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index b00ccfc68c0..e5f827d1e52 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -62,7 +62,7 @@ class TransmissionSensor(SensorEntity): return f"{self._tm_client.api.host}-{self.name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -95,7 +95,7 @@ class TransmissionSpeedSensor(TransmissionSensor): """Representation of a Transmission speed sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return DATA_RATE_MEGABYTES_PER_SECOND @@ -145,7 +145,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "Torrents" diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 22d4e18df5e..5c968b21ed7 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -28,7 +28,8 @@ "limit": "Limit", "order": "Sorrend", "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" - } + }, + "title": "Adja meg az Transmission be\u00e1ll\u00edt\u00e1sokat" } } } diff --git a/homeassistant/components/transmission/translations/zh-Hans.json b/homeassistant/components/transmission/translations/zh-Hans.json index d217ccdc842..a056b99a4bb 100644 --- a/homeassistant/components/transmission/translations/zh-Hans.json +++ b/homeassistant/components/transmission/translations/zh-Hans.json @@ -1,10 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { - "password": "\u5bc6\u7801" - } + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + }, + "title": "\u914d\u7f6e Transmission \u5ba2\u6237\u7aef" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "\u9650\u5236", + "scan_interval": "\u66f4\u65b0\u9891\u7387" + }, + "title": "Transmission \u914d\u7f6e\u9009\u9879" } } } diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index be76999ec3f..0ebb2b39cb8 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -81,7 +81,7 @@ class TransportNSWSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -101,7 +101,7 @@ class TransportNSWSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 82b158aa0ec..c4c68197677 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -113,12 +113,12 @@ class TravisCISensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self._sensor_type][1] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index cc17bf6f1a2..0069c3db93c 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -132,7 +132,7 @@ class TwenteMilieuSensor(SensorEntity): self.async_schedule_update_ha_state(True) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/twentemilieu/translations/hu.json b/homeassistant/components/twentemilieu/translations/hu.json index df83a29ec22..637dadb5baf 100644 --- a/homeassistant/components/twentemilieu/translations/hu.json +++ b/homeassistant/components/twentemilieu/translations/hu.json @@ -4,14 +4,18 @@ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_address": "A c\u00edm nem tal\u00e1lhat\u00f3 a Twente Milieu szolg\u00e1ltat\u00e1si ter\u00fcleten." }, "step": { "user": { "data": { + "house_letter": "H\u00e1zlev\u00e9l/kieg\u00e9sz\u00edt\u0151", "house_number": "h\u00e1zsz\u00e1m", "post_code": "ir\u00e1ny\u00edt\u00f3sz\u00e1m" - } + }, + "description": "\u00c1ll\u00edtsa be a Twente Milieu szolg\u00e1ltat\u00e1st, amely hullad\u00e9kgy\u0171jt\u00e9si inform\u00e1ci\u00f3kat biztos\u00edt a c\u00edm\u00e9re.", + "title": "Twente Milieu" } } } diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index cfabcf1045f..15581e11c28 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -77,7 +77,7 @@ class TwitchSensor(SensorEntity): return self._channel.display_name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index c66db9bb24b..69e4f0df99b 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -93,7 +93,7 @@ class UkTransportSensor(SensorEntity): TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/" _attr_icon = "mdi:train" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, api_app_id, api_app_key, url): """Initialize the sensor.""" @@ -110,7 +110,7 @@ class UkTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 338f695a2b4..6a009415163 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -86,7 +86,7 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN - _attr_unit_of_measurement = DATA_MEGABYTES + _attr_native_unit_of_measurement = DATA_MEGABYTES @property def name(self) -> str: @@ -105,7 +105,7 @@ class UniFiRxBandwidthSensor(UniFiBandwidthSensor): TYPE = RX_SENSOR @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" if self._is_wired: return self.client.wired_rx_bytes / 1000000 @@ -118,7 +118,7 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): TYPE = TX_SENSOR @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" if self._is_wired: return self.client.wired_tx_bytes / 1000000 @@ -167,7 +167,7 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): return f"{super().name} {self.TYPE.capitalize()}" @property - def state(self) -> datetime: + def native_value(self) -> datetime: """Return the uptime of the client.""" if self.client.uptime < 1000000000: return (dt_util.now() - timedelta(seconds=self.client.uptime)).isoformat() diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 5c174e9939d..22904c8ec7b 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -7,7 +7,8 @@ }, "error": { "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service_unavailable": "Sikertelen csatlakoz\u00e1s" + "service_unavailable": "Sikertelen csatlakoz\u00e1s", + "unknown_client_mac": "Nincs el\u00e9rhet\u0151 \u00fcgyf\u00e9l ezen a MAC-c\u00edmen" }, "flow_title": "{site} ({host})", "step": { @@ -28,18 +29,46 @@ "step": { "client_control": { "data": { - "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t" + "block_client": "H\u00e1l\u00f3zathozz\u00e1f\u00e9r\u00e9s vez\u00e9relt \u00fcgyfelek", + "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t", + "poe_clients": "Enged\u00e9lyezze az \u00fcgyfelek POE-vez\u00e9rl\u00e9s\u00e9t" }, - "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st." + "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st.", + "title": "UniFi lehet\u0151s\u00e9gek 2/3" + }, + "device_tracker": { + "data": { + "detection_time": "Id\u0151 m\u00e1sodpercben az utols\u00f3 l\u00e1t\u00e1st\u00f3l a t\u00e1vol tart\u00e1sig", + "ignore_wired_bug": "Az UniFi vezet\u00e9kes hibalogika letilt\u00e1sa", + "ssid_filter": "V\u00e1lassza ki az SSID -ket a vezet\u00e9k n\u00e9lk\u00fcli \u00fcgyfelek nyomon k\u00f6vet\u00e9s\u00e9hez", + "track_clients": "K\u00f6vesse nyomon a h\u00e1l\u00f3zati \u00fcgyfeleket", + "track_devices": "H\u00e1l\u00f3zati eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se (Ubiquiti eszk\u00f6z\u00f6k)", + "track_wired_clients": "Vegyen fel vezet\u00e9kes h\u00e1l\u00f3zati \u00fcgyfeleket" + }, + "description": "Eszk\u00f6zk\u00f6vet\u00e9s konfigur\u00e1l\u00e1sa", + "title": "UniFi lehet\u0151s\u00e9gek 1/3" + }, + "init": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + } }, "simple_options": { + "data": { + "block_client": "H\u00e1l\u00f3zathozz\u00e1f\u00e9r\u00e9s vez\u00e9relt \u00fcgyfelek", + "track_clients": "K\u00f6vesse nyomon a h\u00e1l\u00f3zati \u00fcgyfeleket", + "track_devices": "H\u00e1l\u00f3zati eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se (Ubiquiti eszk\u00f6z\u00f6k)" + }, "description": "UniFi integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra", "allow_uptime_sensors": "\u00dczemid\u0151-\u00e9rz\u00e9kel\u0151k h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" - } + }, + "description": "Statisztikai \u00e9rz\u00e9kel\u0151k konfigur\u00e1l\u00e1sa", + "title": "UniFi lehet\u0151s\u00e9gek 3/3" } } } diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 0ebd9eaf890..2e3e6892c1c 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 diff --git a/homeassistant/components/upb/translations/hu.json b/homeassistant/components/upb/translations/hu.json index b09f497a0e4..58b81af7be8 100644 --- a/homeassistant/components/upb/translations/hu.json +++ b/homeassistant/components/upb/translations/hu.json @@ -5,13 +5,18 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_upb_file": "Hi\u00e1nyz\u00f3 vagy \u00e9rv\u00e9nytelen UPB UPStart export f\u00e1jl, ellen\u0151rizze a f\u00e1jl nev\u00e9t \u00e9s el\u00e9r\u00e9si \u00fatj\u00e1t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { "data": { + "address": "C\u00edm (l\u00e1sd a fenti le\u00edr\u00e1st)", + "file_path": "Az UPStart UPB exportf\u00e1jl el\u00e9r\u00e9si \u00fatja \u00e9s neve.", "protocol": "Protokoll" - } + }, + "description": "Csatlakoztasson egy univerz\u00e1lis Powerline Bus Powerline Interface modult (UPB PIM). A c\u00edmsornak a \u201etcp\u201d \u201ec\u00edm [: port]\u201d form\u00e1tum\u00fanak kell lennie. A port nem k\u00f6telez\u0151, \u00e9s alap\u00e9rtelmezett \u00e9rt\u00e9ke 2101. P\u00e9lda: '192.168.1.42'. A soros protokollhoz a c\u00edmnek 'tty [: baud]' form\u00e1tum\u00fanak kell lennie. A baud opcion\u00e1lis, \u00e9s alap\u00e9rtelmezett \u00e9rt\u00e9ke 4800. P\u00e9lda: '/dev/ttyS1'.", + "title": "Csatlakoz\u00e1s az UPB PIM-hez" } } } diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 9b76c209403..82d42e28589 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -8,11 +8,10 @@ from typing import Any, Dict import requests.exceptions import upcloud_api -import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, @@ -23,18 +22,16 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -58,21 +55,6 @@ SIGNAL_UPDATE_UPCLOUD = "upcloud_update" STATE_MAP = {"error": STATE_PROBLEM, "started": STATE_ON, "stopped": STATE_OFF} -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - class UpCloudDataUpdateCoordinator( DataUpdateCoordinator[Dict[str, upcloud_api.Server]] @@ -115,37 +97,6 @@ class UpCloudHassData: coordinators: dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field( default_factory=dict ) - scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up UpCloud component.""" - domain_config = config.get(DOMAIN) - if not domain_config: - return True - - _LOGGER.warning( - "Loading upcloud via top level config is deprecated and no longer " - "necessary as of 0.117; Please remove it from your YAML configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: domain_config[CONF_USERNAME], - CONF_PASSWORD: domain_config[CONF_PASSWORD], - }, - ) - ) - - if domain_config[CONF_SCAN_INTERVAL]: - hass.data[DATA_UPCLOUD] = UpCloudHassData() - hass.data[DATA_UPCLOUD].scan_interval_migrations[ - domain_config[CONF_USERNAME] - ] = domain_config[CONF_SCAN_INTERVAL] - - return True def _config_entry_update_signal_name(config_entry: ConfigEntry) -> str: @@ -178,22 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Failed to connect", exc_info=True) raise ConfigEntryNotReady from err - upcloud_data = hass.data.setdefault(DATA_UPCLOUD, UpCloudHassData()) - - # Handle pre config entry (0.117) scan interval migration to options - migrated_scan_interval = upcloud_data.scan_interval_migrations.pop( - entry.data[CONF_USERNAME], None - ) - if migrated_scan_interval and ( - not entry.options.get(CONF_SCAN_INTERVAL) - or entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL.total_seconds() - ): - update_interval = migrated_scan_interval - hass.config_entries.async_update_entry( - entry, - options={CONF_SCAN_INTERVAL: update_interval.total_seconds()}, - ) - elif entry.options.get(CONF_SCAN_INTERVAL): + if entry.options.get(CONF_SCAN_INTERVAL): update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL]) else: update_interval = DEFAULT_SCAN_INTERVAL @@ -218,7 +154,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - upcloud_data.coordinators[entry.data[CONF_USERNAME]] = coordinator + hass.data[DATA_UPCLOUD] = UpCloudHassData() + hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_DOMAINS) diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index 1a16a78cfa1..e6868be29b9 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -57,13 +57,6 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import initiated flow.""" - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input=user_input) - @callback def _async_show_form( self, diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 1c6bacede62..25339f6308a 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Home Assistant Updater binary sensors.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntity, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN @@ -18,15 +21,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): """Representation of an updater binary sensor.""" - @property - def name(self) -> str: - """Return the name of the binary sensor, if any.""" - return "Updater" - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return "updater" + _attr_device_class = DEVICE_CLASS_UPDATE + _attr_name = "Updater" + _attr_unique_id = "updater" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 9996d2bb1f0..db225bbf242 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -2,7 +2,6 @@ "domain": "updater", "name": "Updater", "documentation": "https://www.home-assistant.io/integrations/updater", - "requirements": ["distro==1.5.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "cloud_polling" diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 6ad7111ae12..80a7753ec8c 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,6 +1,11 @@ """Open ports in your router for Home Assistant and provide statistics.""" +from __future__ import annotations + import asyncio +from collections.abc import Mapping +from datetime import timedelta from ipaddress import ip_address +from typing import Any import voluptuous as vol @@ -9,28 +14,34 @@ from homeassistant.components import ssdp from homeassistant.components.network import async_get_source_ip from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( CONF_LOCAL_IP, CONFIG_ENTRY_HOSTNAME, + CONFIG_ENTRY_SCAN_INTERVAL, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, + DEFAULT_SCAN_INTERVAL, DOMAIN, DOMAIN_CONFIG, DOMAIN_DEVICES, DOMAIN_LOCAL_IP, - LOGGER as _LOGGER, + LOGGER, ) from .device import Device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" -PLATFORMS = ["sensor"] +PLATFORMS = ["binary_sensor", "sensor"] CONFIG_SCHEMA = vol.Schema( { @@ -44,24 +55,9 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_construct_device(hass: HomeAssistant, udn: str, st: str) -> Device: - """Discovery devices and construct a Device for one.""" - # pylint: disable=invalid-name - _LOGGER.debug("Constructing device: %s::%s", udn, st) - discovery_info = ssdp.async_get_discovery_info_by_udn_st(hass, udn, st) - - if not discovery_info: - _LOGGER.info("Device not discovered") - return None - - return await Device.async_create_device( - hass, discovery_info[ssdp.ATTR_SSDP_LOCATION] - ) - - -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up UPnP component.""" - _LOGGER.debug("async_setup, config: %s", config) + LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = config.get(DOMAIN, conf_default) local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP) @@ -84,26 +80,50 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("Setting up config entry: %s", entry.unique_id) + LOGGER.debug("Setting up config entry: %s", entry.unique_id) - # Discover and construct. udn = entry.data[CONFIG_ENTRY_UDN] st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name + usn = f"{udn}::{st}" + + # Register device discovered-callback. + device_discovered_event = asyncio.Event() + discovery_info: Mapping[str, Any] | None = None + + @callback + def device_discovered(info: Mapping[str, Any]) -> None: + nonlocal discovery_info + LOGGER.debug( + "Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION] + ) + discovery_info = info + device_discovered_event.set() + + cancel_discovered_callback = ssdp.async_register_callback( + hass, + device_discovered, + { + "usn": usn, + }, + ) + try: - device = await async_construct_device(hass, udn, st) + await asyncio.wait_for(device_discovered_event.wait(), timeout=10) except asyncio.TimeoutError as err: + LOGGER.debug("Device not discovered: %s", usn) raise ConfigEntryNotReady from err + finally: + cancel_discovered_callback() - if not device: - _LOGGER.info("Unable to create UPnP/IGD, aborting") - raise ConfigEntryNotReady - - # Save device. - hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device + # Create device. + location = discovery_info[ # pylint: disable=unsubscriptable-object + ssdp.ATTR_SSDP_LOCATION + ] + device = await Device.async_create_device(hass, location) # Ensure entry has a unique_id. if not entry.unique_id: - _LOGGER.debug( + LOGGER.debug( "Setting unique_id: %s, for config_entry: %s", device.unique_id, entry, @@ -134,8 +154,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=device.model_name, ) + update_interval_sec = entry.options.get( + CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + update_interval = timedelta(seconds=update_interval_sec) + LOGGER.debug("update_interval: %s", update_interval) + coordinator = UpnpDataUpdateCoordinator( + hass, + device=device, + update_interval=update_interval, + ) + + # Save coordinator. + hass.data[DOMAIN][entry.entry_id] = coordinator + + await coordinator.async_config_entry_first_refresh() + # Create sensors. - _LOGGER.debug("Enabling sensors") + LOGGER.debug("Enabling sensors") hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Start device updater. @@ -146,14 +182,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a UPnP/IGD device from a config entry.""" - _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) - udn = config_entry.data.get(CONFIG_ENTRY_UDN) - if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: - device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] - await device.async_stop() + if coordinator := hass.data[DOMAIN].pop(config_entry.entry_id, None): + await coordinator.device.async_stop() - del hass.data[DOMAIN][DOMAIN_DEVICES][udn] - - _LOGGER.debug("Deleting sensors") + LOGGER.debug("Deleting sensors") return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + +class UpnpDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to update data from UPNP device.""" + + def __init__( + self, hass: HomeAssistant, device: Device, update_interval: timedelta + ) -> None: + """Initialize.""" + self.device = device + + super().__init__( + hass, LOGGER, name=device.name, update_interval=update_interval + ) + + async def _async_update_data(self) -> Mapping[str, Any]: + """Update data.""" + update_values = await asyncio.gather( + self.device.async_get_traffic_data(), + self.device.async_get_status(), + ) + + data = dict(update_values[0]) + data.update(update_values[1]) + + return data + + +class UpnpEntity(CoordinatorEntity): + """Base class for UPnP/IGD entities.""" + + coordinator: UpnpDataUpdateCoordinator + + def __init__(self, coordinator: UpnpDataUpdateCoordinator) -> None: + """Initialize the base entities.""" + super().__init__(coordinator) + self._device = coordinator.device + self._attr_device_info = { + "connections": {(dr.CONNECTION_UPNP, coordinator.device.udn)}, + "name": coordinator.device.name, + "manufacturer": coordinator.device.manufacturer, + "model": coordinator.device.model_name, + } diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py new file mode 100644 index 00000000000..2f2f0af0e96 --- /dev/null +++ b/homeassistant/components/upnp/binary_sensor.py @@ -0,0 +1,54 @@ +"""Support for UPnP/IGD Binary Sensors.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UpnpDataUpdateCoordinator, UpnpEntity +from .const import DOMAIN, LOGGER, WANSTATUS + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the UPnP/IGD sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + LOGGER.debug("Adding binary sensor") + + sensors = [ + UpnpStatusBinarySensor(coordinator), + ] + async_add_entities(sensors) + + +class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): + """Class for UPnP/IGD binary sensors.""" + + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + + def __init__( + self, + coordinator: UpnpDataUpdateCoordinator, + ) -> None: + """Initialize the base sensor.""" + super().__init__(coordinator) + self._attr_name = f"{coordinator.device.name} wan status" + self._attr_unique_id = f"{coordinator.device.udn}_wanstatus" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.coordinator.data.get(WANSTATUS) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.coordinator.data[WANSTATUS] == "Connected" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 0679d9ffcb5..5df4e267427 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,6 +1,7 @@ """Config flow for UPNP.""" from __future__ import annotations +import asyncio from collections.abc import Mapping from datetime import timedelta from typing import Any @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from .const import ( CONFIG_ENTRY_HOSTNAME, @@ -18,18 +19,69 @@ from .const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, - DOMAIN_DEVICES, - LOGGER as _LOGGER, + LOGGER, + SSDP_SEARCH_TIMEOUT, + ST_IGD_V1, + ST_IGD_V2, ) -from .device import Device, discovery_info_to_discovery + + +def _friendly_name_from_discovery(discovery_info: Mapping[str, Any]) -> str: + """Extract user-friendly name from discovery.""" + return ( + discovery_info.get("friendlyName") + or discovery_info.get("modeName") + or discovery_info.get("_host", "") + ) + + +async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: + """Wait for a device to be discovered.""" + device_discovered_event = asyncio.Event() + + @callback + def device_discovered(info: Mapping[str, Any]) -> None: + LOGGER.info( + "Device discovered: %s, at: %s", + info[ssdp.ATTR_SSDP_USN], + info[ssdp.ATTR_SSDP_LOCATION], + ) + device_discovered_event.set() + + cancel_discovered_callback_1 = ssdp.async_register_callback( + hass, + device_discovered, + { + ssdp.ATTR_SSDP_ST: ST_IGD_V1, + }, + ) + cancel_discovered_callback_2 = ssdp.async_register_callback( + hass, + device_discovered, + { + ssdp.ATTR_SSDP_ST: ST_IGD_V2, + }, + ) + + try: + await asyncio.wait_for( + device_discovered_event.wait(), timeout=SSDP_SEARCH_TIMEOUT + ) + except asyncio.TimeoutError: + return False + finally: + cancel_discovered_callback_1() + cancel_discovered_callback_2() + + return True + + +def _discovery_igd_devices(hass: HomeAssistant) -> list[Mapping[str, Any]]: + """Discovery IGD devices.""" + return ssdp.async_get_discovery_info_by_st( + hass, ST_IGD_V1 + ) + ssdp.async_get_discovery_info_by_st(hass, ST_IGD_V2) class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -50,29 +102,26 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Handle a flow start.""" - _LOGGER.debug("async_step_user: user_input: %s", user_input) + LOGGER.debug("async_step_user: user_input: %s", user_input) if user_input is not None: # Ensure wanted device was discovered. matching_discoveries = [ discovery for discovery in self._discoveries - if discovery[DISCOVERY_UNIQUE_ID] == user_input["unique_id"] + if discovery[ssdp.ATTR_SSDP_USN] == user_input["unique_id"] ] if not matching_discoveries: return self.async_abort(reason="no_devices_found") discovery = matching_discoveries[0] await self.async_set_unique_id( - discovery[DISCOVERY_UNIQUE_ID], raise_on_progress=False + discovery[ssdp.ATTR_SSDP_USN], raise_on_progress=False ) return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = [ - await Device.async_supplement_discovery(self.hass, discovery) - for discovery in await Device.async_discover(self.hass) - ] + discoveries = _discovery_igd_devices(self.hass) # Store discoveries which have not been configured. current_unique_ids = { @@ -81,7 +130,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discoveries = [ discovery for discovery in discoveries - if discovery[DISCOVERY_UNIQUE_ID] not in current_unique_ids + if discovery[ssdp.ATTR_SSDP_USN] not in current_unique_ids ] # Ensure anything to add. @@ -92,7 +141,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required("unique_id"): vol.In( { - discovery[DISCOVERY_UNIQUE_ID]: discovery[DISCOVERY_NAME] + discovery[ssdp.ATTR_SSDP_USN]: _friendly_name_from_discovery( + discovery + ) for discovery in self._discoveries } ), @@ -110,36 +161,36 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): configured before, find any device and create a config_entry for it. Otherwise, do nothing. """ - _LOGGER.debug("async_step_import: import_info: %s", import_info) + LOGGER.debug("async_step_import: import_info: %s", import_info) # Landed here via configuration.yaml entry. # Any device already added, then abort. if self._async_current_entries(): - _LOGGER.debug("Already configured, aborting") + LOGGER.debug("Already configured, aborting") return self.async_abort(reason="already_configured") # Discover devices. - self._discoveries = await Device.async_discover(self.hass) + await _async_wait_for_discoveries(self.hass) + discoveries = _discovery_igd_devices(self.hass) # Ensure anything to add. If not, silently abort. - if not self._discoveries: - _LOGGER.info("No UPnP devices discovered, aborting") + if not discoveries: + LOGGER.info("No UPnP devices discovered, aborting") return self.async_abort(reason="no_devices_found") # Ensure complete discovery. - discovery = self._discoveries[0] + discovery = discoveries[0] if ( - DISCOVERY_UDN not in discovery - or DISCOVERY_ST not in discovery - or DISCOVERY_LOCATION not in discovery - or DISCOVERY_USN not in discovery + ssdp.ATTR_UPNP_UDN not in discovery + or ssdp.ATTR_SSDP_ST not in discovery + or ssdp.ATTR_SSDP_LOCATION not in discovery + or ssdp.ATTR_SSDP_USN not in discovery ): - _LOGGER.debug("Incomplete discovery, ignoring") + LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. - discovery = await Device.async_supplement_discovery(self.hass, discovery) - unique_id = discovery[DISCOVERY_UNIQUE_ID] + unique_id = discovery[ssdp.ATTR_SSDP_USN] await self.async_set_unique_id(unique_id) return await self._async_create_entry_from_discovery(discovery) @@ -150,7 +201,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - _LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) + LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) # Ensure complete discovery. if ( @@ -159,38 +210,31 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or ssdp.ATTR_SSDP_LOCATION not in discovery_info or ssdp.ATTR_SSDP_USN not in discovery_info ): - _LOGGER.debug("Incomplete discovery, ignoring") + LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") - # Convert to something we understand/speak. - discovery = discovery_info_to_discovery(discovery_info) - # Ensure not already configuring/configured. - unique_id = discovery[DISCOVERY_USN] + unique_id = discovery_info[ssdp.ATTR_SSDP_USN] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - updates={CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME]} - ) + hostname = discovery_info["_host"] + self._abort_if_unique_id_configured(updates={CONFIG_ENTRY_HOSTNAME: hostname}) - # Handle devices changing their UDN, only allow a single + # Handle devices changing their UDN, only allow a single host. existing_entries = self._async_current_entries() for config_entry in existing_entries: entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) - if entry_hostname == discovery[DISCOVERY_HOSTNAME]: - _LOGGER.debug( + if entry_hostname == hostname: + LOGGER.debug( "Found existing config_entry with same hostname, discovery ignored" ) return self.async_abort(reason="discovery_ignored") - # Get more data about the device. - discovery = await Device.async_supplement_discovery(self.hass, discovery) - # Store discovery. - self._discoveries = [discovery] + self._discoveries = [discovery_info] # Ensure user recognizable. self.context["title_placeholders"] = { - "name": discovery[DISCOVERY_NAME], + "name": _friendly_name_from_discovery(discovery_info), } return await self.async_step_ssdp_confirm() @@ -199,7 +243,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Confirm integration via SSDP.""" - _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) + LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: return self.async_show_form(step_id="ssdp_confirm") @@ -219,16 +263,16 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery: Mapping, ) -> Mapping[str, Any]: """Create an entry from discovery.""" - _LOGGER.debug( + LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", discovery, ) - title = discovery.get(DISCOVERY_NAME, "") + title = _friendly_name_from_discovery(discovery) data = { - CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], - CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], - CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME], + CONFIG_ENTRY_UDN: discovery["_udn"], + CONFIG_ENTRY_ST: discovery[ssdp.ATTR_SSDP_ST], + CONFIG_ENTRY_HOSTNAME: discovery["_host"], } return self.async_create_entry(title=title, data=data) @@ -243,13 +287,12 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input: Mapping = None) -> None: """Manage the options.""" if user_input is not None: - udn = self.config_entry.data[CONFIG_ENTRY_UDN] - coordinator = self.hass.data[DOMAIN][DOMAIN_DEVICES][udn].coordinator + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] update_interval_sec = user_input.get( CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) update_interval = timedelta(seconds=update_interval_sec) - _LOGGER.debug("Updating coordinator, update_interval: %s", update_interval) + LOGGER.debug("Updating coordinator, update_interval: %s", update_interval) coordinator.update_interval = update_interval return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 0611176350a..769e398c5a4 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -18,17 +18,16 @@ PACKETS_SENT = "packets_sent" TIMESTAMP = "timestamp" DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" +WANSTATUS = "wan_status" +WANIP = "wan_ip" +UPTIME = "uptime" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) -DISCOVERY_HOSTNAME = "hostname" -DISCOVERY_LOCATION = "location" -DISCOVERY_NAME = "name" -DISCOVERY_ST = "st" -DISCOVERY_UDN = "udn" -DISCOVERY_UNIQUE_ID = "unique_id" -DISCOVERY_USN = "usn" CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_HOSTNAME = "hostname" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() +ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" +ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" +SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index cf76aa41f8a..ca06f501405 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -12,7 +12,6 @@ from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.device_updater import DeviceUpdater from async_upnp_client.profiles.igd import IgdDevice -from homeassistant.components import ssdp from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -22,36 +21,18 @@ from .const import ( BYTES_RECEIVED, BYTES_SENT, CONF_LOCAL_IP, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, DOMAIN_CONFIG, LOGGER as _LOGGER, PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, + UPTIME, + WANIP, + WANSTATUS, ) -def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: - """Convert a SSDP-discovery to 'our' discovery.""" - location = discovery_info[ssdp.ATTR_SSDP_LOCATION] - parsed = urlparse(location) - hostname = parsed.hostname - return { - DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], - DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], - DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], - DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], - DISCOVERY_HOSTNAME: hostname, - } - - def _get_local_ip(hass: HomeAssistant) -> IPv4Address | None: """Get the configured local ip.""" if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: @@ -70,29 +51,6 @@ class Device: self._device_updater = device_updater self.coordinator: DataUpdateCoordinator = None - @classmethod - async def async_discover(cls, hass: HomeAssistant) -> list[Mapping]: - """Discover UPnP/IGD devices.""" - _LOGGER.debug("Discovering UPnP/IGD devices") - discoveries = [] - for ssdp_st in IgdDevice.DEVICE_TYPES: - for discovery_info in ssdp.async_get_discovery_info_by_st(hass, ssdp_st): - discoveries.append(discovery_info_to_discovery(discovery_info)) - return discoveries - - @classmethod - async def async_supplement_discovery( - cls, hass: HomeAssistant, discovery: Mapping - ) -> Mapping: - """Get additional data from device and supplement discovery.""" - location = discovery[DISCOVERY_LOCATION] - device = await Device.async_create_device(hass, location) - discovery[DISCOVERY_NAME] = device.name - discovery[DISCOVERY_HOSTNAME] = device.hostname - discovery[DISCOVERY_UNIQUE_ID] = discovery[DISCOVERY_USN] - - return discovery - @classmethod async def async_create_device( cls, hass: HomeAssistant, ssdp_location: str @@ -199,3 +157,18 @@ class Device: PACKETS_RECEIVED: values[2], PACKETS_SENT: values[3], } + + async def async_get_status(self) -> Mapping[str, Any]: + """Get connection status, uptime, and external IP.""" + _LOGGER.debug("Getting status for device: %s", self) + + values = await asyncio.gather( + self._igd_device.async_get_status_info(), + self._igd_device.async_get_external_ip_address(), + ) + + return { + WANSTATUS: values[0][0] if values[0] is not None else None, + UPTIME: values[0][2] if values[0] is not None else None, + WANIP: values[1], + } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 41d50b4bae8..5f38a827ec7 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,9 +3,9 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.19.1"], + "requirements": ["async-upnp-client==0.20.0"], "dependencies": ["network", "ssdp"], - "codeowners": ["@StevenLooman"], + "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 54744490a86..185d3ecac6d 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,38 +1,25 @@ """Support for UPnP/IGD Sensors.""" from __future__ import annotations -from datetime import timedelta -from typing import Any, Mapping - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.core import HomeAssistant -from homeassistant.helpers import 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 UpnpDataUpdateCoordinator, UpnpEntity from .const import ( BYTES_RECEIVED, BYTES_SENT, - CONFIG_ENTRY_SCAN_INTERVAL, - CONFIG_ENTRY_UDN, DATA_PACKETS, DATA_RATE_PACKETS_PER_SECOND, - DEFAULT_SCAN_INTERVAL, DOMAIN, - DOMAIN_DEVICES, KIBIBYTE, - LOGGER as _LOGGER, + LOGGER, PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, ) -from .device import Device SENSOR_TYPES = { BYTES_RECEIVED: { @@ -78,7 +65,7 @@ async def async_setup_platform( hass: HomeAssistant, config, async_add_entities, discovery_info=None ) -> None: """Old way of setting up UPnP/IGD sensors.""" - _LOGGER.debug( + LOGGER.debug( "async_setup_platform: config: %s, discovery: %s", config, discovery_info ) @@ -89,52 +76,36 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the UPnP/IGD sensors.""" - udn = config_entry.data[CONFIG_ENTRY_UDN] - device: Device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] + coordinator = hass.data[DOMAIN][config_entry.entry_id] - update_interval_sec = config_entry.options.get( - CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - update_interval = timedelta(seconds=update_interval_sec) - _LOGGER.debug("update_interval: %s", update_interval) - _LOGGER.debug("Adding sensors") - coordinator = DataUpdateCoordinator[Mapping[str, Any]]( - hass, - _LOGGER, - name=device.name, - update_method=device.async_get_traffic_data, - update_interval=update_interval, - ) - device.coordinator = coordinator - - await coordinator.async_refresh() + LOGGER.debug("Adding sensors") sensors = [ - RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]), + RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]), + RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]), + RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]), + RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]), ] - async_add_entities(sensors, True) + async_add_entities(sensors) -class UpnpSensor(CoordinatorEntity, SensorEntity): +class UpnpSensor(UpnpEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" def __init__( self, - coordinator: DataUpdateCoordinator[Mapping[str, Any]], - device: Device, - sensor_type: Mapping[str, str], + coordinator: UpnpDataUpdateCoordinator, + sensor_type: dict[str, str], ) -> None: """Initialize the base sensor.""" super().__init__(coordinator) - self._device = device self._sensor_type = sensor_type + self._attr_name = f"{coordinator.device.name} {sensor_type['name']}" + self._attr_unique_id = f"{coordinator.device.udn}_{sensor_type['unique_id']}" @property def icon(self) -> str: @@ -144,43 +115,21 @@ class UpnpSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: """Return if entity is available.""" - device_value_key = self._sensor_type["device_value_key"] - return ( - self.coordinator.last_update_success - and device_value_key in self.coordinator.data + return super().available and self.coordinator.data.get( + self._sensor_type["device_value_key"] ) @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device.name} {self._sensor_type['name']}" - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._device.udn}_{self._sensor_type['unique_id']}" - - @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._sensor_type["unit"] - @property - def device_info(self) -> DeviceInfo: - """Get device info.""" - return { - "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, - "name": self._device.name, - "manufacturer": self._device.manufacturer, - "model": self._device.model_name, - } - class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" device_value_key = self._sensor_type["device_value_key"] value = self.coordinator.data[device_value_key] @@ -192,24 +141,18 @@ class RawUpnpSensor(UpnpSensor): class DerivedUpnpSensor(UpnpSensor): """Representation of a UNIT Sent/Received per second sensor.""" - def __init__(self, coordinator, device, sensor_type) -> None: + def __init__(self, coordinator: UpnpDataUpdateCoordinator, sensor_type) -> None: """Initialize sensor.""" - super().__init__(coordinator, device, sensor_type) + super().__init__(coordinator, sensor_type) self._last_value = None self._last_timestamp = None + self._attr_name = f"{coordinator.device.name} {sensor_type['derived_name']}" + self._attr_unique_id = ( + f"{coordinator.device.udn}_{sensor_type['derived_unique_id']}" + ) @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device.name} {self._sensor_type['derived_name']}" - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._device.udn}_{self._sensor_type['derived_unique_id']}" - - @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._sensor_type["derived_unit"] @@ -218,7 +161,7 @@ class DerivedUpnpSensor(UpnpSensor): return current_value < self._last_value @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" # Can't calculate any derivative if we have only one value. device_value_key = self._sensor_type["device_value_key"] diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 49756babc8b..8ef3ff8dcc0 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "incomplete_discovery": "Hi\u00e1nyos felfedez\u00e9s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { @@ -10,8 +11,16 @@ }, "flow_title": "{name}", "step": { + "init": { + "one": "\u00dcres", + "other": "" + }, + "ssdp_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt az UPnP/IGD eszk\u00f6zt?" + }, "user": { "data": { + "scan_interval": "Friss\u00edt\u00e9si intervallum (m\u00e1sodperc, minimum 30)", "unique_id": "Eszk\u00f6z", "usn": "Eszk\u00f6z" } diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 5b31b2e81d0..db06b09ea18 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -50,4 +50,4 @@ class UptimeSensor(SensorEntity): self._attr_name: str = name self._attr_device_class: str = DEVICE_CLASS_TIMESTAMP self._attr_should_poll: bool = False - self._attr_state: str = dt_util.now().isoformat() + self._attr_native_value: str = dt_util.now().isoformat() diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 17bc8f9a629..4eaef45c4d2 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -1,12 +1,23 @@ """The Uptime Robot integration.""" from __future__ import annotations -from pyuptimerobot import UptimeRobot, UptimeRobotException, UptimeRobotMonitor +from pyuptimerobot import ( + UptimeRobot, + UptimeRobotAuthenticationException, + UptimeRobotException, + UptimeRobotMonitor, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import ( + DeviceRegistry, + async_entries_for_config_entry, + async_get_registry, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLATFORMS @@ -18,25 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: uptime_robot_api = UptimeRobot( entry.data[CONF_API_KEY], async_get_clientsession(hass) ) + dev_reg = await async_get_registry(hass) - async def async_update_data() -> list[UptimeRobotMonitor]: - """Fetch data from API UptimeRobot API.""" - try: - response = await uptime_robot_api.async_get_monitors() - except UptimeRobotException as exception: - raise UpdateFailed(exception) from exception - else: - if response.status == API_ATTR_OK: - monitors: list[UptimeRobotMonitor] = response.data - return monitors - raise UpdateFailed(response.error.message) - - hass.data[DOMAIN][entry.entry_id] = coordinator = DataUpdateCoordinator( + hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( hass, - LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=COORDINATOR_UPDATE_INTERVAL, + config_entry_id=entry.entry_id, + dev_reg=dev_reg, + api=uptime_robot_api, ) await coordinator.async_config_entry_first_refresh() @@ -53,3 +52,64 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for Uptime Robot.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + dev_reg: DeviceRegistry, + api: UptimeRobot, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_method=self._async_update_data, + update_interval=COORDINATOR_UPDATE_INTERVAL, + ) + self._config_entry_id = config_entry_id + self._device_registry = dev_reg + self._api = api + + async def _async_update_data(self) -> list[UptimeRobotMonitor] | None: + """Update data.""" + try: + response = await self._api.async_get_monitors() + except UptimeRobotAuthenticationException as exception: + raise ConfigEntryAuthFailed(exception) from exception + except UptimeRobotException as exception: + raise UpdateFailed(exception) from exception + else: + if response.status != API_ATTR_OK: + raise UpdateFailed(response.error.message) + + monitors: list[UptimeRobotMonitor] = response.data + + current_monitors = { + list(device.identifiers)[0][1] + for device in async_entries_for_config_entry( + self._device_registry, self._config_entry_id + ) + } + new_monitors = {str(monitor.id) for monitor in monitors} + if stale_monitors := current_monitors - new_monitors: + for monitor_id in stale_monitors: + if device := self._device_registry.async_get_device( + {(DOMAIN, monitor_id)} + ): + self._device_registry.async_remove_device(device.id) + + # If there are new monitors, we should reload the config entry so we can + # create new devices and entities. + if self.data and new_monitors - {str(monitor.id) for monitor in self.data}: + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._config_entry_id) + ) + return None + + return monitors diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index f99689f2507..ac0dc0c1186 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -53,7 +53,7 @@ async def async_setup_entry( name=monitor.friendly_name, device_class=DEVICE_CLASS_CONNECTIVITY, ), - target=monitor.url, + monitor=monitor, ) for monitor in coordinator.data ], diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 7bab74fa03e..1e8bec992ad 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -58,15 +58,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if response and response.data and response.data.email else None ) - if account: - await self.async_set_unique_id(str(account.user_id)) - self._abort_if_unique_id_configured() return errors, account async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle the initial step.""" - errors: dict[str, str] = {} if user_input is None: return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA @@ -74,12 +70,48 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors, account = await self._validate_input(user_input) if account: + await self.async_set_unique_id(str(account.user_id)) + self._abort_if_unique_id_configured() return self.async_create_entry(title=account.email, data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + async def async_step_reauth( + self, user_input: ConfigType | None = None + ) -> FlowResult: + """Return the reauth confirm step.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: ConfigType | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA + ) + errors, account = await self._validate_input(user_input) + if account: + if self.context.get("unique_id") and self.context["unique_id"] != str( + account.user_id + ): + errors["base"] = "reauth_failed_matching_account" + else: + existing_entry = await self.async_set_unique_id(str(account.user_id)) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_failed_existing") + + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + async def async_step_import(self, import_config: ConfigType) -> FlowResult: """Import a config entry from configuration.yaml.""" for entry in self._async_current_entries(): @@ -93,5 +125,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _, account = await self._validate_input(imported_config) if account: + await self.async_set_unique_id(str(account.user_id)) + self._abort_if_unique_id_configured() return self.async_create_entry(title=account.email, data=imported_config) return self.async_abort(reason="unknown") diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index ee9832a040a..7f3655b75cf 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -7,7 +7,8 @@ from typing import Final LOGGER: Logger = getLogger(__package__) -COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=60) +# The free plan is limited to 10 requests/minute +COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" PLATFORMS: Final = ["binary_sensor"] diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 4b4847dfc7c..89ff7680eae 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -20,59 +20,43 @@ class UptimeRobotEntity(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, description: EntityDescription, - target: str, + monitor: UptimeRobotMonitor, ) -> None: """Initialize Uptime Robot entities.""" super().__init__(coordinator) self.entity_description = description - self._target = target + self._monitor = monitor + self._attr_device_info = { + "identifiers": {(DOMAIN, str(self.monitor.id))}, + "name": self.monitor.friendly_name, + "manufacturer": "Uptime Robot Team", + "entry_type": "service", + "model": self.monitor.type.name, + } self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_TARGET: self._target, + ATTR_TARGET: self.monitor.url, } + self._attr_unique_id = str(self.monitor.id) @property - def unique_id(self) -> str | None: - """Return the unique_id of the entity.""" - return str(self.monitor.id) if self.monitor else None - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - if self.monitor: - return { - "identifiers": {(DOMAIN, str(self.monitor.id))}, - "name": "Uptime Robot", - "manufacturer": "Uptime Robot Team", - "entry_type": "service", - "model": self.monitor.type.name, - } - return {} - - @property - def monitors(self) -> list[UptimeRobotMonitor]: + def _monitors(self) -> list[UptimeRobotMonitor]: """Return all monitors.""" return self.coordinator.data or [] @property - def monitor(self) -> UptimeRobotMonitor | None: + def monitor(self) -> UptimeRobotMonitor: """Return the monitor for this entity.""" return next( ( monitor - for monitor in self.monitors + for monitor in self._monitors if str(monitor.id) == self.entity_description.key ), - None, + self._monitor, ) @property def monitor_available(self) -> bool: """Returtn if the monitor is available.""" - status: bool = self.monitor.status == 2 if self.monitor else False - return status - - @property - def available(self) -> bool: - """Returtn if entity is available.""" - return self.monitor is not None + return bool(self.monitor.status == 2) diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 22d9a6d9477..279bf6eb43e 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -8,6 +8,7 @@ "codeowners": [ "@ludeeus" ], + "quality_scale": "platinum", "iot_class": "cloud_polling", "config_flow": true } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index f51061eec33..094130b470d 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -1,20 +1,31 @@ { - "config": { - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "config": { + "step": { + "user": { + "description": "You need to supply a read-only API key from Uptime Robot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "You need to supply a new read-only API key from Uptime Robot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "unknown": "[%key:common::config_flow::error::unknown%]" } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "unknown": "[%key:common::config_flow::error::unknown%]" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json new file mode 100644 index 00000000000..a3bccb98295 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_failed_existing": "No s'ha pogut actualitzar l'entrada de configuraci\u00f3, elimina la integraci\u00f3 i torna-la a instal\u00b7lar.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "reauth_failed_matching_account": "La clau API proporcionada no correspon amb l'identificador del compte de la configuraci\u00f3 actual.", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Has de proporcionar una nova clau API de nom\u00e9s lectura d'Uptime Robot", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Has de proporcionar una clau API de nom\u00e9s lectura d'Uptime Robot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/cs.json b/homeassistant/components/uptimerobot/translations/cs.json index 7261d6146fb..09480693834 100644 --- a/homeassistant/components/uptimerobot/translations/cs.json +++ b/homeassistant/components/uptimerobot/translations/cs.json @@ -1,17 +1,30 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_failed_existing": "Nepoda\u0159ilo se aktualizovat polo\u017eku konfigurace, odstra\u0148te pros\u00edm integraci a nastavte ji znovu.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "reauth_failed_matching_account": "Zadan\u00fd kl\u00ed\u010d API neodpov\u00edd\u00e1 ID \u00fa\u010dtu pro st\u00e1vaj\u00edc\u00ed konfiguraci.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, + "description": "Je t\u0159eba zadat nov\u00fd kl\u00ed\u010d API \u010dten\u00ed od spole\u010dnosti Uptime Robot.", + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API" - } + }, + "description": "Mus\u00edte zadat kl\u00ed\u010d API pro \u010dten\u00ed od spole\u010dnosti Uptime Robot." } } } diff --git a/homeassistant/components/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json new file mode 100644 index 00000000000..a25f58dfe0c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_failed_existing": "Der Konfigurationseintrag konnte nicht aktualisiert werden. Bitte entferne die Integration und richte sie erneut ein.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "reauth_failed_matching_account": "Der von dir angegebene API-Schl\u00fcssel stimmt nicht mit der Konto-ID f\u00fcr die vorhandene Konfiguration \u00fcberein.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Du musst einen neuen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen.", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Du musst einen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index 99ab9426006..ae1a8cf5e45 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -1,19 +1,30 @@ { "config": { "abort": { - "already_configured": "Account already configured", + "already_configured": "Account is already configured", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", + "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration.", "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "You need to supply a new read-only API key from Uptime Robot", + "title": "Reauthenticate Integration" + }, "user": { "data": { "api_key": "API Key" - } + }, + "description": "You need to supply a read-only API key from Uptime Robot" } } } diff --git a/homeassistant/components/uptimerobot/translations/es-419.json b/homeassistant/components/uptimerobot/translations/es-419.json new file mode 100644 index 00000000000..445247107c0 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json new file mode 100644 index 00000000000..d3c7f2b036d --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", + "unknown": "Error desconocido" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave de la API err\u00f3nea", + "reauth_failed_matching_account": "La clave de API que proporcion\u00f3 no coincide con el ID de cuenta para la configuraci\u00f3n existente.", + "unknown": "Error desconocido" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "Debe proporcionar una nueva clave API de solo lectura de Uptime Robot", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave de la API" + }, + "description": "Debe proporcionar una clave API de solo lectura de robot de tiempo de actividad/funcionamiento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/et.json b/homeassistant/components/uptimerobot/translations/et.json new file mode 100644 index 00000000000..c679ea3b19b --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_failed_existing": "Seadekirjet ei \u00f5nnestunud uuendada, eemalda sidumine ja seadista see uuesti.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "reauth_failed_matching_account": "Sisestatud API v\u00f5ti ei vasta olemasoleva konto ID s\u00e4tetele.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Pead sisestama uue Uptime Roboti kirjutuskaitstud API-v\u00f5tme", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Pead sisestama Uptime Roboti kirjutuskaitstud API-v\u00f5tme" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json new file mode 100644 index 00000000000..2b4322bb410 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "reauth_failed_existing": "Impossible de mettre \u00e0 jour l'entr\u00e9e de configuration, veuillez supprimer l'int\u00e9gration et la configurer \u00e0 nouveau.", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "Echec de la connexion", + "invalid_api_key": "Cl\u00e9 API non valide", + "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous devez fournir une nouvelle cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous devez fournir une cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/he.json b/homeassistant/components/uptimerobot/translations/he.json new file mode 100644 index 00000000000..07de294aea4 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/he.json @@ -0,0 +1,27 @@ +{ + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \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": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json new file mode 100644 index 00000000000..000851093a5 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_api_key": "\u00c9rv\u00e9nytelen API-kulcs", + "reauth_failed_matching_account": "A megadott API -kulcs nem egyezik a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 fi\u00f3kazonos\u00edt\u00f3j\u00e1val.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "Meg kell adnia egy \u00faj, csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l", + "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs" + }, + "description": "Meg kell adnia egy csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json new file mode 100644 index 00000000000..517bbf6463f --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_failed_existing": "Impossibile aggiornare la voce di configurazione, rimuovere l'integrazione e configurarla di nuovo.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "reauth_failed_matching_account": "La chiave API che hai fornito non corrisponde all'ID account per la configurazione esistente.", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Devi fornire una nuova chiave API di sola lettura da Uptime Robot", + "title": "Autenticare nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API" + }, + "description": "Devi fornire una chiave API di sola lettura da Uptime Robot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index 3a77fedf228..7e0ad6a3cd0 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -1,17 +1,30 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_failed_existing": "Kon de config entry niet updaten, gelieve de integratie te verwijderen en het opnieuw op te zetten.", + "reauth_successful": "Herauthenticatie was succesvol", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "reauth_failed_matching_account": "De API sleutel die u heeft opgegeven komt niet overeen met de account ID voor de bestaande configuratie.", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "api_key": "API-sleutel" - } + }, + "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven" } } } diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json new file mode 100644 index 00000000000..8c6351d78c4 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "reauth_failed_matching_account": "API-n\u00f8kkelen du oppgav, samsvarer ikke med konto-IDen for eksisterende konfigurasjon.", + "unknown": "Uventet feil" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Du m\u00e5 angi en ny skrivebeskyttet API-n\u00f8kkel fra Uptime Robot", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Du m\u00e5 angi en skrivebeskyttet API-n\u00f8kkel fra Uptime Robot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/pl.json b/homeassistant/components/uptimerobot/translations/pl.json new file mode 100644 index 00000000000..ac413226e98 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/pt.json b/homeassistant/components/uptimerobot/translations/pt.json new file mode 100644 index 00000000000..10c16aafa0f --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/ru.json b/homeassistant/components/uptimerobot/translations/ru.json new file mode 100644 index 00000000000..88da4b3b768 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/ru.json @@ -0,0 +1,31 @@ +{ + "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.", + "reauth_failed_existing": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430.", + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "reauth_failed_matching_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\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": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/zh-Hans.json b/homeassistant/components/uptimerobot/translations/zh-Hans.json new file mode 100644 index 00000000000..d680c09e967 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/zh-Hans.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e", + "reauth_failed_existing": "\u65e0\u6cd5\u66f4\u65b0\u914d\u7f6e\u6761\u76ee\uff0c\u8bf7\u5220\u9664\u96c6\u6210\u5e76\u91cd\u65b0\u8bbe\u7f6e\u3002", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "reauth_failed_matching_account": "\u60a8\u63d0\u4f9b\u7684 API \u5bc6\u94a5\u4e0e\u73b0\u6709\u914d\u7f6e\u7684\u8d26\u53f7 ID \u4e0d\u5339\u914d", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u94a5" + }, + "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u94a5" + }, + "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json new file mode 100644 index 00000000000..73d27aac1db --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_failed_existing": "\u7121\u6cd5\u66f4\u65b0\u8a2d\u5b9a\u5be6\u9ad4\uff0c\u8acb\u79fb\u9664\u6574\u5408\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "reauth_failed_matching_account": "\u6240\u63d0\u4f9b\u7684\u5bc6\u9470\u8207\u73fe\u6709\u8a2d\u5b9a\u5e33\u865f ID \u4e0d\u7b26\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u5bc6\u9470", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u552f\u8b80 API \u5bc6\u9470" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index bd261aba4fb..c0c2d1ae165 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -54,7 +54,7 @@ class UscisSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 509c0562f97..84533efdcf5 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,11 +5,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - STATE_CLASS_MEASUREMENT, - SensorEntity, -) +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -58,6 +54,7 @@ ATTR_SOURCE_ID = "source" ATTR_STATUS = "status" ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" +ATTR_LAST_RESET = "last_reset" ATTR_TARIFF = "tariff" DEVICE_CLASS_MAP = { @@ -321,7 +318,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -336,7 +333,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return STATE_CLASS_MEASUREMENT @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -352,6 +349,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, + ATTR_LAST_RESET: self._last_reset.isoformat(), } if self._period is not None: state_attr[ATTR_PERIOD] = self._period @@ -363,8 +361,3 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def icon(self): """Return the icon to use in the frontend, if any.""" return ICON - - @property - def last_reset(self): - """Return the time when the sensor was last reset.""" - return self._last_reset diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 74bf175f75a..77ff6a30f95 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -194,10 +194,12 @@ class UnifiVideoCamera(Camera): self._caminfo = caminfo return True - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return the image of this camera.""" if not self._camera and not self._login(): - return + return None def _get_image(retry=True): try: diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index ddfb9d1a7d3..dd669e156cf 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, @@ -44,6 +44,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=None, unit_of_measurement=PERCENTAGE, icon="mdi:fan", + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Extract Air", @@ -52,6 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Exhaust Air", @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Outdoor Air", @@ -68,6 +71,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Supply Air", @@ -76,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Humidity", @@ -84,6 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_HUMIDITY, unit_of_measurement=PERCENTAGE, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxFilterRemainingSensor( name=f"{name} Remaining Time For Filter", @@ -100,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=None, unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} CO2", @@ -108,6 +115,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_CO2, unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ] @@ -118,13 +126,21 @@ class ValloxSensor(SensorEntity): """Representation of a Vallox sensor.""" def __init__( - self, name, state_proxy, metric_key, device_class, unit_of_measurement, icon + self, + name, + state_proxy, + metric_key, + device_class, + unit_of_measurement, + icon, + state_class=None, ) -> None: """Initialize the Vallox sensor.""" self._name = name self._state_proxy = state_proxy self._metric_key = metric_key self._device_class = device_class + self._state_class = state_class self._unit_of_measurement = unit_of_measurement self._icon = icon self._available = None @@ -141,7 +157,7 @@ class ValloxSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @@ -150,6 +166,11 @@ class ValloxSensor(SensorEntity): """Return the device class.""" return self._device_class + @property + def state_class(self): + """Return the state class.""" + return self._state_class + @property def icon(self): """Return the icon.""" @@ -161,7 +182,7 @@ class ValloxSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 31c5da097ff..4c1c1de5e52 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -110,7 +110,7 @@ class VasttrafikDepartureSensor(SensorEntity): return self._attributes @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 9d9b68dd4eb..3a4aa2302f6 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -45,14 +45,14 @@ class VelbusSensor(VelbusEntity, SensorEntity): return self._module.get_class(self._channel) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._is_counter: return self._module.get_counter_state(self._channel) return self._module.get_state(self._channel) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" if self._is_counter: return self._module.get_counter_unit(self._channel) diff --git a/homeassistant/components/velbus/translations/hu.json b/homeassistant/components/velbus/translations/hu.json index 414ee7e60c6..6bf3ba689f3 100644 --- a/homeassistant/components/velbus/translations/hu.json +++ b/homeassistant/components/velbus/translations/hu.json @@ -6,6 +6,15 @@ "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "name": "Ennek a velbus kapcsolatnak a neve", + "port": "Kapcsolati karakterl\u00e1nc" + }, + "title": "Hat\u00e1rozza meg a velbus kapcsolat t\u00edpus\u00e1t" + } } } } \ No newline at end of file diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index feac63f694b..9a153841718 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp @@ -63,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: """Set up for Vera controllers.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 878f6ff376d..dd6d891c11d 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -53,12 +53,12 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def state(self) -> str: + def native_value(self) -> str: """Return the name of the sensor.""" return self.current_value @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: diff --git a/homeassistant/components/vera/translations/hu.json b/homeassistant/components/vera/translations/hu.json new file mode 100644 index 00000000000..1f1e22b9ed8 --- /dev/null +++ b/homeassistant/components/vera/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban.", + "vera_controller_url": "Vez\u00e9rl\u0151 URL" + }, + "description": "Adja meg a Vera vez\u00e9rl\u0151 URL-j\u00e9t al\u00e1bb. Ennek \u00edgy kell kin\u00e9znie: http://192.168.1.161:3480.", + "title": "Vera vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban." + }, + "description": "Az opcion\u00e1lis param\u00e9terekr\u0151l a vera dokument\u00e1ci\u00f3j\u00e1ban olvashat: https://www.home-assistant.io/integrations/vera/. Megjegyz\u00e9s: Az itt v\u00e9grehajtott v\u00e1ltoztat\u00e1sokhoz \u00fajra kell ind\u00edtani a h\u00e1zi asszisztens szervert. Az \u00e9rt\u00e9kek t\u00f6rl\u00e9s\u00e9hez adjon meg egy sz\u00f3k\u00f6zt.", + "title": "Vera vez\u00e9rl\u0151 opci\u00f3k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a137f61d98f..455d7070a8b 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -79,7 +79,9 @@ class VerisureSmartcam(CoordinatorEntity, Camera): "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), } - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" self.check_imagelist() if not self._image: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index d39c235e9d5..cdeddd8d6e4 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -51,7 +51,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -84,7 +84,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["climate"][self.serial_number]["temperature"] @@ -104,7 +104,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -137,7 +137,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["climate"][self.serial_number]["humidity"] @@ -156,7 +156,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator - _attr_unit_of_measurement = "Mice" + _attr_native_unit_of_measurement = "Mice" def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -186,7 +186,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["mice"][self.serial_number]["detections"] diff --git a/homeassistant/components/verisure/translations/zh-Hans.json b/homeassistant/components/verisure/translations/zh-Hans.json new file mode 100644 index 00000000000..e786edb1405 --- /dev/null +++ b/homeassistant/components/verisure/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index d29032af399..50982e92d12 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -65,12 +65,12 @@ class VSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 04165ec9db1..1cd42cce9b3 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -2,11 +2,19 @@ from datetime import timedelta import logging -from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource -from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException +from pyhaversion import ( + HaVersion, + HaVersionChannel, + HaVersionSource, + exceptions as pyhaversionexceptions, +) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import CONF_NAME, CONF_SOURCE from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -30,12 +38,10 @@ ALL_IMAGES = [ "raspberrypi4", "tinker", ] -ALL_SOURCES = [ - "container", - "haio", - "local", - "pypi", - "supervisor", + +HA_VERSION_SOURCES = [source.value for source in HaVersionSource] + +ALL_SOURCES = HA_VERSION_SOURCES + [ "hassio", # Kept to not break existing configurations "docker", # Kept to not break existing configurations ] @@ -48,8 +54,6 @@ DEFAULT_NAME_LATEST = "Latest Version" DEFAULT_NAME_LOCAL = "Current Version" DEFAULT_SOURCE = "local" -ICON = "mdi:package-up" - TIME_BETWEEN_UPDATES = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -72,40 +76,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) source = config.get(CONF_SOURCE) + channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE session = async_get_clientsession(hass) - channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE + if source in HA_VERSION_SOURCES: + source = HaVersionSource(source) + elif source == "hassio": + source = HaVersionSource.SUPERVISOR + elif source == "docker": + source = HaVersionSource.CONTAINER - if source == "pypi": - haversion = VersionData( - HaVersion(session, source=HaVersionSource.PYPI, channel=channel) - ) - elif source in ["hassio", "supervisor"]: - haversion = VersionData( - HaVersion( - session, source=HaVersionSource.SUPERVISOR, channel=channel, image=image - ) - ) - elif source in ["docker", "container"]: - if image is not None and image != DEFAULT_IMAGE: - image = f"{image}-homeassistant" - haversion = VersionData( - HaVersion( - session, source=HaVersionSource.CONTAINER, channel=channel, image=image - ) - ) - elif source == "haio": - haversion = VersionData(HaVersion(session, source=HaVersionSource.HAIO)) - else: - haversion = VersionData(HaVersion(session, source=HaVersionSource.LOCAL)) + if ( + source in (HaVersionSource.SUPERVISOR, HaVersionSource.CONTAINER) + and image is not None + and image != DEFAULT_IMAGE + ): + image = f"{image}-homeassistant" - if not name: - if source == DEFAULT_SOURCE: + if not (name := config.get(CONF_NAME)): + if source == HaVersionSource.LOCAL: name = DEFAULT_NAME_LOCAL else: name = DEFAULT_NAME_LATEST - async_add_entities([VersionSensor(haversion, name)], True) + async_add_entities( + [ + VersionSensor( + VersionData( + HaVersion( + session=session, source=source, image=image, channel=channel + ) + ), + SensorEntityDescription(key=source, name=name), + ) + ], + True, + ) class VersionData: @@ -120,9 +126,9 @@ class VersionData: """Get the latest version information.""" try: await self.api.get_version() - except HaVersionFetchException as exception: + except pyhaversionexceptions.HaVersionFetchException as exception: _LOGGER.warning(exception) - except HaVersionParseException as exception: + except pyhaversionexceptions.HaVersionParseException as exception: _LOGGER.warning( "Could not parse data received for %s - %s", self.api.source, exception ) @@ -131,32 +137,19 @@ class VersionData: class VersionSensor(SensorEntity): """Representation of a Home Assistant version sensor.""" - def __init__(self, data: VersionData, name: str) -> None: + _attr_icon = "mdi:package-up" + + def __init__( + self, + data: VersionData, + description: SensorEntityDescription, + ) -> None: """Initialize the Version sensor.""" self.data = data - self._name = name - self._state = None + self.entity_description = description async def async_update(self): """Get the latest version information.""" await self.data.async_update() - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self.data.api.version - - @property - def extra_state_attributes(self): - """Return attributes for the sensor.""" - return self.data.api.version_data - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON + self._attr_native_value = self.data.api.version + self._attr_extra_state_attributes = self.data.api.version_data 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 diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 10821859f9a..ddfbb9f20dd 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -103,7 +103,7 @@ class ViaggiaTrenoSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -113,7 +113,7 @@ class ViaggiaTrenoSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 4f7ab9df985..e96b3b8120a 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -369,12 +369,12 @@ class ViCareSensor(SensorEntity): return self._sensor[CONF_ICON] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor[CONF_UNIT_OF_MEASUREMENT] diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index 90527c60458..bb2df21f257 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -72,7 +72,7 @@ class VilfoRouterSensor(SensorEntity): return f"{parent_device_name} {sensor_name}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -82,7 +82,7 @@ class VilfoRouterSensor(SensorEntity): return self._unique_id @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return SENSOR_TYPES[self.sensor_type].get(ATTR_UNIT) diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index 34db9cf7cc9..4e2ab47a476 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -14,6 +14,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Vilfo Router integr\u00e1ci\u00f3t. Sz\u00fcks\u00e9ge van a Vilfo Router gazdag\u00e9pnev\u00e9re/IP -c\u00edm\u00e9re \u00e9s egy API hozz\u00e1f\u00e9r\u00e9si jogkivonatra. Ha tov\u00e1bbi inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ge az integr\u00e1ci\u00f3r\u00f3l \u00e9s a r\u00e9szletekr\u0151l, l\u00e1togasson el a k\u00f6vetkez\u0151 webhelyre: https://www.home-assistant.io/integrations/vilfo", "title": "Csatlakoz\u00e1s a Vilfo routerhez" } } diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 953d64f0ff6..b813d337e82 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -1,4 +1,6 @@ """Support for Vivotek IP Cameras.""" +from __future__ import annotations + from libpyvivotek import VivotekCamera import voluptuous as vol @@ -87,7 +89,9 @@ class VivotekCam(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" return self._cam.snapshot() diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 6f0962509f5..edc91cdb31c 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -6,16 +6,25 @@ "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "complete_pairing_failed": "Nem siker\u00fclt befejezni a p\u00e1ros\u00edt\u00e1st. Az \u00fajb\u00f3li elk\u00fcld\u00e9s el\u0151tt gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott PIN-k\u00f3d helyes, a TV tov\u00e1bbra is be van kapcsolva, \u00e9s csatlakozik a h\u00e1l\u00f3zathoz.", + "existing_config_entry_found": "Egy megl\u00e9v\u0151 VIZIO SmartCast Eszk\u00f6z konfigur\u00e1ci\u00f3s bejegyz\u00e9s ugyanazzal a sorozatsz\u00e1mmal m\u00e1r konfigur\u00e1lva van. Ennek konfigur\u00e1l\u00e1s\u00e1hoz t\u00f6r\u00f6lnie kell a megl\u00e9v\u0151 bejegyz\u00e9st." }, "step": { "pair_tv": { "data": { "pin": "PIN-k\u00f3d" - } + }, + "description": "A TV-nek k\u00f3dot kell megjelen\u00edtenie. \u00cdrja be ezt a k\u00f3dot az \u0171rlapba, majd folytassa a k\u00f6vetkez\u0151 l\u00e9p\u00e9ssel a p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez.", + "title": "V\u00e9gezze el a p\u00e1ros\u00edt\u00e1si folyamatot" }, "pairing_complete": { - "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz." + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz.", + "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" + }, + "pairing_complete_import": { + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant szolg\u00e1ltat\u00e1shoz. \n\n A Hozz\u00e1f\u00e9r\u00e9si token a \u201e** {access_token} **\u201d.", + "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" }, "user": { "data": { @@ -24,6 +33,7 @@ "host": "Hoszt", "name": "N\u00e9v" }, + "description": "A Hozz\u00e1f\u00e9r\u00e9si token csak t\u00e9v\u00e9khez sz\u00fcks\u00e9ges. Ha TV -t konfigur\u00e1l, \u00e9s m\u00e9g nincs Hozz\u00e1f\u00e9r\u00e9si token , hagyja \u00fcresen a p\u00e1ros\u00edt\u00e1si folyamathoz.", "title": "VIZIO SmartCast Eszk\u00f6z" } } @@ -32,8 +42,11 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Alkalmaz\u00e1sok felv\u00e9telre vagy kiz\u00e1r\u00e1sra", + "include_or_exclude": "Alkalmaz\u00e1sok felv\u00e9tele vagy kiz\u00e1r\u00e1sa?", "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" }, + "description": "Ha rendelkezik Smart TV-vel, opcion\u00e1lisan sz\u0171rheti a forr\u00e1slist\u00e1t \u00fagy, hogy kiv\u00e1lasztja, mely alkalmaz\u00e1sokat k\u00edv\u00e1nja felvenni vagy kiz\u00e1rni a forr\u00e1slist\u00e1b\u00f3l.", "title": "VIZIO SmartCast Eszk\u00f6z be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } diff --git a/homeassistant/components/vizio/translations/zh-Hans.json b/homeassistant/components/vizio/translations/zh-Hans.json new file mode 100644 index 00000000000..1fa1ebc751d --- /dev/null +++ b/homeassistant/components/vizio/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "pair_tv": { + "title": "\u5b8c\u6210\u914d\u5bf9\u8fc7\u7a0b" + }, + "pairing_complete": { + "title": "\u914d\u5bf9\u5b8c\u6210" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 4eb2f512f31..21705d494d9 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -97,7 +97,7 @@ class VolkszaehlerSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.type][1] @@ -107,7 +107,7 @@ class VolkszaehlerSensor(SensorEntity): return self.vz_api.available @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index ad6571576b4..7a37713301e 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -15,11 +15,11 @@ class VolvoSensor(VolvoEntity, SensorEntity): """Representation of a Volvo sensor.""" @property - def state(self): + def native_value(self): """Return the state.""" return self.instrument.state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.instrument.unit diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 5e6815944d7..01506d4f47e 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -92,12 +92,12 @@ class VultrSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement to present the value in.""" return self._units @property - def state(self): + def native_value(self): """Return the value of this given sensor type.""" try: return round(float(self.data.get(self._condition)), 2) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 6d3ef952cbe..0691a39ff48 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -1,6 +1,6 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config, async_add_entities): ) -class WallboxSensor(CoordinatorEntity, Entity): +class WallboxSensor(CoordinatorEntity, SensorEntity): """Representation of the Wallbox portal.""" def __init__(self, coordinator, idx, ent, config): @@ -46,12 +46,12 @@ class WallboxSensor(CoordinatorEntity, Entity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data[self._ent] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of the sensor.""" return self._unit diff --git a/homeassistant/components/wallbox/translations/zh-Hans.json b/homeassistant/components/wallbox/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/wallbox/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 084ec17bb28..ed6013daa74 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -129,7 +129,7 @@ class WaqiSensor(SensorEntity): return "mdi:cloud" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._data is not None: return self._data.get("aqi") @@ -146,7 +146,7 @@ class WaqiSensor(SensorEntity): return self.uid @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "AQI" diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 8691cc4ed02..5d7832ca58d 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -101,7 +101,7 @@ class WaterFurnaceSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -111,7 +111,7 @@ class WaterFurnaceSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index b0168bbb44e..43265062998 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -202,7 +202,7 @@ class WazeTravelTime(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._waze_data.duration is not None: return round(self._waze_data.duration) @@ -210,7 +210,7 @@ class WazeTravelTime(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return TIME_MINUTES diff --git a/homeassistant/components/weather/translations/ru.json b/homeassistant/components/weather/translations/ru.json index d2d0a066874..b0f92257631 100644 --- a/homeassistant/components/weather/translations/ru.json +++ b/homeassistant/components/weather/translations/ru.json @@ -1,13 +1,13 @@ { "state": { "_": { - "clear-night": "\u042f\u0441\u043d\u043e, \u043d\u043e\u0447\u044c", + "clear-night": "\u042f\u0441\u043d\u043e", "cloudy": "\u041e\u0431\u043b\u0430\u0447\u043d\u043e", "exceptional": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435", "fog": "\u0422\u0443\u043c\u0430\u043d", "hail": "\u0413\u0440\u0430\u0434", - "lightning": "\u041c\u043e\u043b\u043d\u0438\u044f", - "lightning-rainy": "\u041c\u043e\u043b\u043d\u0438\u044f, \u0434\u043e\u0436\u0434\u044c", + "lightning": "\u0413\u0440\u043e\u0437\u0430", + "lightning-rainy": "\u0414\u043e\u0436\u0434\u044c \u0441 \u0433\u0440\u043e\u0437\u043e\u0439", "partlycloudy": "\u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0441\u0442\u044c", "pouring": "\u041b\u0438\u0432\u0435\u043d\u044c", "rainy": "\u0414\u043e\u0436\u0434\u044c", diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 60d42e97604..d6f27aff6ae 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -53,12 +53,12 @@ class APICount(SensorEntity): return "Connected clients" @property - def state(self) -> int: + def native_value(self) -> int: """Return current API count.""" return self.count @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return "clients" diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 21a7760741a..59eae24c714 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.6"], + "requirements": ["pywemo==0.6.7"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index ebd68231e0c..f1f32e8b909 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,10 +1,11 @@ """Support for power sensors in WeMo Insight devices.""" import asyncio -from datetime import datetime, timedelta +from datetime import timedelta from typing import Callable from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -16,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import StateType -from homeassistant.util import Throttle, convert, dt +from homeassistant.util import Throttle, convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoSubscriptionEntity @@ -98,7 +99,7 @@ class InsightCurrentPower(InsightSensor): ) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the current power consumption.""" return ( convert(self.wemo.insight_params[self.entity_description.key], float, 0.0) @@ -113,17 +114,12 @@ class InsightTodayEnergy(InsightSensor): key="todaymw", name="Today Energy", device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) @property - def last_reset(self) -> datetime: - """Return the time when the sensor was initialized.""" - return dt.start_of_local_day() - - @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the current energy use today.""" miliwatts = convert( self.wemo.insight_params[self.entity_description.key], float, 0.0 diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json index bcb2f438353..ff9f4dc5f75 100644 --- a/homeassistant/components/wemo/translations/hu.json +++ b/homeassistant/components/wemo/translations/hu.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Wemo-t?" + } } }, "device_automation": { diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 4219c80193d..5d5e595fa50 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -70,12 +70,12 @@ class WhoisSensor(SensorEntity): return "mdi:calendar-clock" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement to present the value in.""" return TIME_DAYS @property - def state(self): + def native_value(self): """Return the expiration days for hostname.""" return self._state diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index 800a420f8f0..b9bcd317b46 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -78,12 +78,12 @@ class NumberEntity(WiffiEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the value of the entity.""" return self._value @@ -111,7 +111,7 @@ class StringEntity(WiffiEntity, SensorEntity): self.reset_expiration_date() @property - def state(self): + def native_value(self): """Return the value of the entity.""" return self._value diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json index c623f6ddaba..902fabcbc85 100644 --- a/homeassistant/components/wiffi/translations/hu.json +++ b/homeassistant/components/wiffi/translations/hu.json @@ -1,13 +1,15 @@ { "config": { "abort": { + "addr_in_use": "A szerverport m\u00e1r haszn\u00e1latban van.", "start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt." }, "step": { "user": { "data": { "port": "Port" - } + }, + "title": "TCP szerver be\u00e1ll\u00edt\u00e1sa WIFFI eszk\u00f6z\u00f6kh\u00f6z" } } }, diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index f11e15670e9..f346d9145f8 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -111,25 +111,28 @@ CHIME_TONES = TONES + ["inactive"] AUTO_SHUTOFF_TIMES = [None, -1, 30, 60, 120] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Inclusive( - CONF_EMAIL, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_PASSWORD, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Inclusive( + CONF_EMAIL, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_PASSWORD, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean, + } + ), + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -282,6 +285,10 @@ def _request_oauth_completion(hass, config): def setup(hass, config): # noqa: C901 """Set up the Wink component.""" + _LOGGER.warning( + "The Wink integration has been deprecated and is pending removal in " + "Home Assistant Core 2021.11" + ) if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index f640a24def2..86199f44e91 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -62,7 +62,7 @@ class WinkSensorEntity(WinkDevice, SensorEntity): self.hass.data[DOMAIN]["entities"]["sensor"].append(self) @property - def state(self): + def native_value(self): """Return the state.""" state = None if self.capability == "humidity": @@ -82,7 +82,7 @@ class WinkSensorEntity(WinkDevice, SensorEntity): return state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index de70efda424..7ad0a7f52c2 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -83,7 +83,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): return self.name.lower().replace(" ", "_") @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -93,7 +93,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): return self._sensor_type @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor.unit diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index ca7391eb58e..0ca40d28440 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -30,11 +30,11 @@ class WithingsHealthSensor(BaseWithingsSensor, SensorEntity): """Implementation of a Withings sensor.""" @property - def state(self) -> None | str | int | float: + def native_value(self) -> None | str | int | float: """Return the state of the entity.""" return self._state_data @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._attribute.unit_of_measurement diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index ec8c628a485..e26cff027fc 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -25,6 +25,7 @@ "title": "Felhaszn\u00e1l\u00f3i profil." }, "reauth": { + "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 634f903c020..48d8443a0a9 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -48,7 +48,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): """Defines a WLED estimated current sensor.""" _attr_icon = "mdi:power" - _attr_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE + _attr_native_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE _attr_device_class = DEVICE_CLASS_CURRENT def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -66,7 +66,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): } @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.leds.power @@ -84,7 +84,7 @@ class WLEDUptimeSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_uptime" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() @@ -95,7 +95,7 @@ class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:memory" _attr_entity_registry_enabled_default = False - _attr_unit_of_measurement = DATA_BYTES + _attr_native_unit_of_measurement = DATA_BYTES def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" @@ -104,7 +104,7 @@ class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_free_heap" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.free_heap @@ -113,7 +113,7 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi signal sensor.""" _attr_icon = "mdi:wifi" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_registry_enabled_default = False def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -123,7 +123,7 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -134,7 +134,7 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi RSSI sensor.""" _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT _attr_entity_registry_enabled_default = False def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -144,7 +144,7 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -164,7 +164,7 @@ class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -184,7 +184,7 @@ class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 0d35d4bce5c..92f18e04de4 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -63,7 +63,7 @@ class WolfLinkSensor(CoordinatorEntity, SensorEntity): return f"{self.wolf_object.name}" @property - def state(self): + def native_value(self): """Return the state. Wolf Client is returning only changed values so we need to store old value here.""" if self.wolf_object.parameter_id in self.coordinator.data: new_state = self.coordinator.data[self.wolf_object.parameter_id] @@ -95,7 +95,7 @@ class WolfLinkHours(WolfLinkSensor): return "mdi:clock" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TIME_HOURS @@ -109,7 +109,7 @@ class WolfLinkTemperature(WolfLinkSensor): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TEMP_CELSIUS @@ -123,7 +123,7 @@ class WolfLinkPressure(WolfLinkSensor): return DEVICE_CLASS_PRESSURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PRESSURE_BAR @@ -132,7 +132,7 @@ class WolfLinkPercentage(WolfLinkSensor): """Class for percentage based entities.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.wolf_object.unit @@ -146,7 +146,7 @@ class WolfLinkState(WolfLinkSensor): return "wolflink__state" @property - def state(self): + def native_value(self): """Return the state converting with supported values.""" state = super().state resolved_state = [ diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json index c7bb483155d..79d03d91034 100644 --- a/homeassistant/components/wolflink/translations/hu.json +++ b/homeassistant/components/wolflink/translations/hu.json @@ -12,13 +12,15 @@ "device": { "data": { "device_name": "Eszk\u00f6z" - } + }, + "title": "V\u00e1lassza ki a WOLF eszk\u00f6zt" }, "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "WOLF SmartSet kapcsolat" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json index b393660f35a..0a257e570cf 100644 --- a/homeassistant/components/wolflink/translations/sensor.hu.json +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -1,9 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "F\u00fcstg\u00e1zcsillap\u00edt\u00f3", + "absenkbetrieb": "Visszaes\u00e9s m\u00f3d", + "absenkstop": "Visszaes\u00e9s meg\u00e1ll\u00edt\u00e1sa", + "aktiviert": "Aktiv\u00e1lt", + "antilegionellenfunktion": "Anti-legionella funkci\u00f3", + "at_abschaltung": "OT le\u00e1ll\u00edt\u00e1s", + "at_frostschutz": "OT fagyv\u00e9delem", + "aus": "Letiltva", + "auto": "Automatikus", "auto_off_cool": "AutomataKiH\u0171t\u00e9s", + "auto_on_cool": "AutomatikusH\u0171t\u00e9s", "automatik_aus": "Automatikus kikapcsol\u00e1s", - "permanent": "\u00c1lland\u00f3" + "automatik_ein": "Automatikus bekapcsol\u00e1s", + "bereit_keine_ladung": "K\u00e9sz, nincs bet\u00f6ltve", + "betrieb_ohne_brenner": "Munka \u00e9g\u0151 n\u00e9lk\u00fcl", + "cooling": "H\u0171t\u00e9s", + "deaktiviert": "Inakt\u00edv", + "dhw_prior": "DHW Priorit\u00e1s", + "eco": "Takar\u00e9kos", + "ein": "Enged\u00e9lyezve", + "estrichtrocknung": "Padl\u00f3sz\u00e1r\u00edt\u00e1si", + "externe_deaktivierung": "K\u00fcls\u0151 deaktiv\u00e1l\u00e1s", + "fernschalter_ein": "T\u00e1vir\u00e1ny\u00edt\u00f3 enged\u00e9lyezve", + "frost_heizkreis": "F\u0171t\u0151k\u00f6r fagy\u00e1s", + "frost_warmwasser": "DHW fagy", + "frostschutz": "Fagyv\u00e9delem", + "gasdruck": "G\u00e1znyom\u00e1s", + "glt_betrieb": "BMS m\u00f3d", + "gradienten_uberwachung": "\u00c1tmenet monitoroz\u00e1s", + "heizbetrieb": "F\u0171t\u00e9si m\u00f3d", + "heizgerat_mit_speicher": "Kaz\u00e1n hengerrel", + "heizung": "F\u0171t\u00e9s", + "initialisierung": "Inicializ\u00e1l\u00e1s", + "kalibration": "Kalibr\u00e1ci\u00f3", + "kalibration_heizbetrieb": "F\u0171t\u00e9si m\u00f3d kalibr\u00e1l\u00e1sa", + "kalibration_kombibetrieb": "Kombin\u00e1lt m\u00f3d kalibr\u00e1l\u00e1sa", + "kalibration_warmwasserbetrieb": "DHW kalibr\u00e1l\u00e1s", + "kaskadenbetrieb": "Kaszk\u00e1d m\u0171k\u00f6d\u00e9s", + "kombibetrieb": "Kombin\u00e1lt m\u00f3d", + "kombigerat": "Kombin\u00e1lt kaz\u00e1n", + "kombigerat_mit_solareinbindung": "Kombin\u00e1lt kaz\u00e1n napelemes integr\u00e1ci\u00f3val", + "mindest_kombizeit": "Minim\u00e1lis kombin\u00e1lt id\u0151", + "nachlauf_heizkreispumpe": "A f\u0171t\u0151k\u00f6r szivatty\u00fa bej\u00e1rat\u00e1sa", + "nachspulen": "Ut\u00f3\u00f6bl\u00edt\u00e9s", + "nur_heizgerat": "Csak kaz\u00e1n", + "parallelbetrieb": "P\u00e1rhuzamos \u00fczemm\u00f3d", + "partymodus": "Party m\u00f3d", + "perm_cooling": "\u00c1lland\u00f3H\u0171t\u00e9s", + "permanent": "\u00c1lland\u00f3", + "permanentbetrieb": "\u00c1lland\u00f3 \u00fczemm\u00f3d", + "reduzierter_betrieb": "Korl\u00e1tozott m\u00f3d", + "rt_abschaltung": "RT le\u00e1ll\u00edt\u00e1s", + "rt_frostschutz": "RT fagyv\u00e9delem", + "ruhekontakt": "Pihen\u0151 kapcsolat", + "schornsteinfeger": "Emisszi\u00f3s vizsg\u00e1lat", + "smart_grid": "SmartGrid", + "smart_home": "OkosOtthon", + "softstart": "L\u00e1gy ind\u00edt\u00e1s", + "solarbetrieb": "Napenergia \u00fczemm\u00f3d", + "sparbetrieb": "Gazdas\u00e1gos m\u00f3d", + "sparen": "Gazdas\u00e1gos", + "spreizung_hoch": "dT t\u00fal sz\u00e9les", + "spreizung_kf": "Spread KF", + "stabilisierung": "Stabiliz\u00e1ci\u00f3", + "standby": "K\u00e9szenl\u00e9t", + "start": "Indul\u00e1s", + "storung": "Hiba", + "taktsperre": "Anti-ciklus", + "telefonfernschalter": "Telefonos t\u00e1vkapcsol\u00f3", + "test": "Teszt", + "tpw": "TPW", + "urlaubsmodus": "Nyaral\u00e1s \u00fczemm\u00f3d", + "ventilprufung": "Szelep teszt", + "vorspulen": "Bel\u00e9p\u00e9si sz\u00e1r\u00edt\u00e1s", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW gyorsind\u00edt\u00e1s", + "warmwasserbetrieb": "DHW m\u00f3d", + "warmwassernachlauf": "DHW befut\u00e1s", + "warmwasservorrang": "DHW priorit\u00e1s", + "zunden": "Gy\u00fajt\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index de5b3991e3f..74da12f7f61 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -54,7 +54,7 @@ class WorldClockSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 0fa65957e40..4d7a32605b0 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -88,7 +88,7 @@ class WorldTidesInfoSensor(SensorEntity): return attr @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.data: if "High" in str(self.data["extremes"][0]["type"]): diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index e7600670c52..b34481d0990 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -68,12 +68,12 @@ class WorxLandroidSensor(SensorEntity): return f"worxlandroid-{self.sensor}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the sensor.""" if self.sensor == "battery": return PERCENTAGE diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 153d496a7d6..bc0023ac54f 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -88,7 +88,7 @@ class WashingtonStateTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -96,7 +96,7 @@ class WashingtonStateTransportSensor(SensorEntity): class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 13cd4217b4d..5ca9e4ef6f7 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -369,7 +369,7 @@ class XBeeDigitalOut(XBeeDigitalIn): class XBeeAnalogIn(SensorEntity): """Representation of a GPIO pin configured as an analog input.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, config, device): """Initialize the XBee analog in device.""" @@ -416,7 +416,7 @@ class XBeeAnalogIn(SensorEntity): return self._config.should_poll @property - def state(self): + def sensor_state(self): """Return the state of the entity.""" return self._value diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index b1d5ece7d57..8dae25ad5e1 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -47,7 +47,7 @@ class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, config, device): """Initialize the sensor.""" @@ -61,7 +61,7 @@ class XBeeTemperatureSensor(SensorEntity): return self._config.name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._temp diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e651cdbcf3..d54d79532ca 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import api, config_flow @@ -50,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the xbox component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 9aa0de4a727..854c0b007f6 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -33,7 +33,7 @@ class XboxSensorEntity(XboxBaseSensorEntity, SensorEntity): """Representation of a Xbox presence state.""" @property - def state(self): + def native_value(self): """Return the state of the requested attribute.""" if not self.coordinator.last_update_success: return None diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index 2717bc1ad62..c09b707cba0 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -98,7 +98,7 @@ class XboxSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index d6f313c0382..049b4bfcbc0 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,4 +1,6 @@ """Support for Xeoma Cameras.""" +from __future__ import annotations + import logging from pyxeoma.xeoma import Xeoma, XeomaError @@ -109,7 +111,9 @@ class XeomaCamera(Camera): self._password = password self._last_image = None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 359d6c8b896..016fe7dd2ba 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,12 +1,13 @@ """This component provides support for Xiaomi Cameras.""" -import asyncio +from __future__ import annotations + from ftplib import FTP, error_perm import logging from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol +from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( @@ -138,7 +139,9 @@ class XiaomiCamera(Camera): return f"ftp://{self.user}:{self.passwd}@{host}:{self.port}{ftp.pwd()}/{video}" - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: @@ -149,11 +152,12 @@ class XiaomiCamera(Camera): url = await self.hass.async_add_executor_job(self.get_latest_video_url, host) if url != self._last_url: - ffmpeg = ImageFrame(self._manager.binary) - self._last_image = await asyncio.shield( - ffmpeg.get_image( - url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments - ) + self._last_image = await ffmpeg.async_get_image( + self.hass, + url, + extra_cmd=self._extra_arguments, + width=width, + height=height, ) self._last_url = url diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index cc49bb14251..cad3afb11ba 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -125,7 +125,7 @@ class XiaomiSensor(XiaomiDevice, SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" try: return SENSOR_TYPES.get(self._data_key)[0] @@ -142,7 +142,7 @@ class XiaomiSensor(XiaomiDevice, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -176,7 +176,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return PERCENTAGE @@ -186,7 +186,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index bc87f461c33..469fa14bcc1 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -33,7 +33,7 @@ "data": { "host": "IP-Adresse (optional)", "interface": "Die zu verwendende Netzwerkschnittstelle", - "mac": "MAC-Adresse" + "mac": "MAC-Adresse (optional)" }, "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", "title": "Xiaomi Aqara Gateway" diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 89355ae309e..9d854607213 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -3,7 +3,15 @@ from datetime import timedelta import logging import async_timeout -from miio import AirHumidifier, AirHumidifierMiot, AirHumidifierMjjsq, DeviceException +from miio import ( + AirFresh, + AirHumidifier, + AirHumidifierMiot, + AirHumidifierMjjsq, + AirPurifier, + AirPurifierMiot, + DeviceException, +) from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core @@ -24,9 +32,11 @@ from .const import ( MODELS_AIR_MONITOR, MODELS_FAN, MODELS_HUMIDIFIER, + MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, MODELS_LIGHT, + MODELS_PURIFIER_MIOT, MODELS_SWITCH, MODELS_VACUUM, ) @@ -36,8 +46,15 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] -FAN_PLATFORMS = ["fan"] -HUMIDIFIER_PLATFORMS = ["humidifier", "number", "select", "sensor", "switch"] +FAN_PLATFORMS = ["fan", "select", "sensor"] +HUMIDIFIER_PLATFORMS = [ + "binary_sensor", + "humidifier", + "number", + "select", + "sensor", + "switch", +] LIGHT_PLATFORMS = ["light"] VACUUM_PLATFORMS = ["vacuum"] AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] @@ -100,27 +117,48 @@ async def async_create_miio_device_and_coordinator( token = entry.data[CONF_TOKEN] name = entry.title device = None + migrate = False - if model not in MODELS_HUMIDIFIER: + if model not in MODELS_HUMIDIFIER and model not in MODELS_FAN: return _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + # Humidifiers if model in MODELS_HUMIDIFIER_MIOT: device = AirHumidifierMiot(host, token) + migrate = True elif model in MODELS_HUMIDIFIER_MJJSQ: device = AirHumidifierMjjsq(host, token, model=model) - else: + migrate = True + elif model in MODELS_HUMIDIFIER_MIIO: device = AirHumidifier(host, token, model=model) + migrate = True + # Airpurifiers and Airfresh + elif model in MODELS_PURIFIER_MIOT: + device = AirPurifierMiot(host, token) + elif model.startswith("zhimi.airpurifier."): + device = AirPurifier(host, token) + elif model.startswith("zhimi.airfresh."): + device = AirFresh(host, token) + else: + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/syssi/xiaomi_airpurifier/issues " + "and provide the following data: %s", + model, + ) + return - # 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 - 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) + if migrate: + # 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 + 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(): """Fetch data from the device using async_add_executor_job.""" diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py new file mode 100644 index 00000000000..6254c00916e --- /dev/null +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -0,0 +1,114 @@ +"""Support for Xiaomi Miio binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Callable + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, + BinarySensorEntityDescription, +) + +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + MODELS_HUMIDIFIER_MIIO, + MODELS_HUMIDIFIER_MIOT, + MODELS_HUMIDIFIER_MJJSQ, +) +from .device import XiaomiCoordinatedMiioEntity + +ATTR_NO_WATER = "no_water" +ATTR_WATER_TANK_DETACHED = "water_tank_detached" + + +@dataclass +class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): + """A class that describes binary sensor entities.""" + + value: Callable | None = None + + +BINARY_SENSOR_TYPES = ( + XiaomiMiioBinarySensorDescription( + key=ATTR_NO_WATER, + name="Water Tank Empty", + icon="mdi:water-off-outline", + ), + XiaomiMiioBinarySensorDescription( + key=ATTR_WATER_TANK_DETACHED, + name="Water Tank", + icon="mdi:car-coolant-level", + device_class=DEVICE_CLASS_CONNECTIVITY, + value=lambda value: not value, + ), +) + +HUMIDIFIER_MIIO_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) +HUMIDIFIER_MIOT_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) +HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi sensor from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + model = config_entry.data[CONF_MODEL] + sensors = [] + if model in MODELS_HUMIDIFIER_MIIO: + sensors = HUMIDIFIER_MIIO_BINARY_SENSORS + elif model in MODELS_HUMIDIFIER_MIOT: + sensors = HUMIDIFIER_MIOT_BINARY_SENSORS + elif model in MODELS_HUMIDIFIER_MJJSQ: + sensors = HUMIDIFIER_MJJSQ_BINARY_SENSORS + for description in BINARY_SENSOR_TYPES: + if description.key not in sensors: + continue + entities.append( + XiaomiGenericBinarySensor( + f"{config_entry.title} {description.name}", + hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], + config_entry, + f"{description.key}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, + ) + ) + + async_add_entities(entities) + + +class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): + """Representation of a Xiaomi Humidifier binary sensor.""" + + def __init__(self, name, device, entry, unique_id, coordinator, description): + """Initialize the entity.""" + super().__init__(name, device, entry, unique_id, coordinator) + + self.entity_description = description + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + state = self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.key + ) + if self.entity_description.value is not None and state is not None: + return self.entity_description.value(state) + + return state + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c407e92a6ae..184629fa2fb 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -63,7 +63,7 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, ] -MODELS_FAN_MIIO = [ +MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V2, MODEL_AIRPURIFIER_V3, @@ -124,7 +124,7 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", ] -MODELS_FAN = MODELS_FAN_MIIO + MODELS_PURIFIER_MIOT +MODELS_FAN = MODELS_PURIFIER_MIIO + MODELS_PURIFIER_MIOT MODELS_HUMIDIFIER = ( MODELS_HUMIDIFIER_MIOT + MODELS_HUMIDIFIER_MIIO + MODELS_HUMIDIFIER_MJJSQ ) @@ -159,10 +159,8 @@ SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" SERVICE_SET_FAN_LED_OFF = "fan_set_led_off" SERVICE_SET_FAN_LED = "fan_set_led" -SERVICE_SET_LED_BRIGHTNESS = "set_led_brightness" SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" -SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" SERVICE_SET_FAN_LEVEL = "fan_set_fan_level" SERVICE_SET_AUTO_DETECT_ON = "fan_set_auto_detect_on" @@ -226,7 +224,6 @@ FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_LEARN_MODE | FEATURE_RESET_FILTER @@ -261,7 +258,6 @@ FEATURE_FLAGS_AIRPURIFIER_3 = ( | FEATURE_SET_LED | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_FAN_LEVEL - | FEATURE_SET_LED_BRIGHTNESS ) FEATURE_FLAGS_AIRPURIFIER_V3 = ( @@ -269,10 +265,7 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( ) FEATURE_FLAGS_AIRHUMIDIFIER = ( - FEATURE_SET_BUZZER - | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED_BRIGHTNESS - | FEATURE_SET_TARGET_HUMIDITY + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_TARGET_HUMIDITY ) FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY @@ -284,7 +277,6 @@ FEATURE_FLAGS_AIRHUMIDIFIER_MJSSQ = ( FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_TARGET_HUMIDITY | FEATURE_SET_DRY | FEATURE_SET_MOTOR_SPEED @@ -295,7 +287,6 @@ FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_RESET_FILTER | FEATURE_SET_EXTRA_FEATURES ) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c58d9ad0c66..87e8fa0ca2a 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,23 +1,12 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" import asyncio from enum import Enum -from functools import partial import logging import math -from miio import AirFresh, AirPurifier, AirPurifierMiot, DeviceException -from miio.airfresh import ( - LedBrightness as AirfreshLedBrightness, - OperationMode as AirfreshOperationMode, -) -from miio.airpurifier import ( - LedBrightness as AirpurifierLedBrightness, - OperationMode as AirpurifierOperationMode, -) -from miio.airpurifier_miot import ( - LedBrightness as AirpurifierMiotLedBrightness, - OperationMode as AirpurifierMiotOperationMode, -) +from miio.airfresh import OperationMode as AirfreshOperationMode +from miio.airpurifier import OperationMode as AirpurifierOperationMode +from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode import voluptuous as vol from homeassistant.components.fan import ( @@ -30,11 +19,11 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_TOKEN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -54,8 +43,9 @@ from .const import ( FEATURE_SET_FAVORITE_LEVEL, FEATURE_SET_LEARN_MODE, FEATURE_SET_LED, - FEATURE_SET_LED_BRIGHTNESS, FEATURE_SET_VOLUME, + KEY_COORDINATOR, + KEY_DEVICE, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_PRO, @@ -77,11 +67,9 @@ from .const import ( SERVICE_SET_FAVORITE_LEVEL, SERVICE_SET_LEARN_MODE_OFF, SERVICE_SET_LEARN_MODE_ON, - SERVICE_SET_LED_BRIGHTNESS, SERVICE_SET_VOLUME, - SUCCESS, ) -from .device import XiaomiMiioEntity +from .device import XiaomiCoordinatedMiioEntity _LOGGER = logging.getLogger(__name__) @@ -103,26 +91,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_MODEL = "model" # Air Purifier -ATTR_HUMIDITY = "humidity" -ATTR_AIR_QUALITY_INDEX = "aqi" -ATTR_FILTER_HOURS_USED = "filter_hours_used" ATTR_FILTER_LIFE = "filter_life_remaining" ATTR_FAVORITE_LEVEL = "favorite_level" ATTR_BUZZER = "buzzer" ATTR_CHILD_LOCK = "child_lock" ATTR_LED = "led" -ATTR_LED_BRIGHTNESS = "led_brightness" -ATTR_MOTOR_SPEED = "motor_speed" -ATTR_AVERAGE_AIR_QUALITY_INDEX = "average_aqi" -ATTR_PURIFY_VOLUME = "purify_volume" ATTR_BRIGHTNESS = "brightness" ATTR_LEVEL = "level" ATTR_FAN_LEVEL = "fan_level" -ATTR_MOTOR2_SPEED = "motor2_speed" -ATTR_ILLUMINANCE = "illuminance" -ATTR_FILTER_RFID_PRODUCT_ID = "filter_rfid_product_id" -ATTR_FILTER_RFID_TAG = "filter_rfid_tag" -ATTR_FILTER_TYPE = "filter_type" ATTR_LEARN_MODE = "learn_mode" ATTR_SLEEP_TIME = "sleep_time" ATTR_SLEEP_LEARN_COUNT = "sleep_mode_learn_count" @@ -135,22 +111,12 @@ ATTR_VOLUME = "volume" ATTR_USE_TIME = "use_time" ATTR_BUTTON_PRESSED = "button_pressed" -# Air Fresh -ATTR_CO2 = "co2" - # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { - ATTR_TEMPERATURE: "temperature", - ATTR_HUMIDITY: "humidity", - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", ATTR_FAVORITE_LEVEL: "favorite_level", ATTR_CHILD_LOCK: "child_lock", ATTR_LED: "led", - ATTR_MOTOR_SPEED: "motor_speed", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", ATTR_LEARN_MODE: "learn_mode", ATTR_EXTRA_FEATURES: "extra_features", ATTR_TURBO_MODE_SUPPORTED: "turbo_mode_supported", @@ -159,27 +125,18 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { AVAILABLE_ATTRIBUTES_AIRPURIFIER = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", ATTR_AUTO_DETECT: "auto_detect", ATTR_USE_TIME: "use_time", ATTR_BUZZER: "buzzer", - ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_SLEEP_MODE: "sleep_mode", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_USE_TIME: "use_time", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", - ATTR_MOTOR2_SPEED: "motor2_speed", ATTR_VOLUME: "volume", - # perhaps supported but unconfirmed ATTR_AUTO_DETECT: "auto_detect", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", @@ -187,64 +144,31 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", - ATTR_MOTOR2_SPEED: "motor2_speed", ATTR_VOLUME: "volume", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, ATTR_BUZZER: "buzzer", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 = { - ATTR_TEMPERATURE: "temperature", - ATTR_HUMIDITY: "humidity", - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", ATTR_FAVORITE_LEVEL: "favorite_level", ATTR_CHILD_LOCK: "child_lock", ATTR_LED: "led", - ATTR_MOTOR_SPEED: "motor_speed", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_USE_TIME: "use_time", ATTR_BUZZER: "buzzer", - ATTR_LED_BRIGHTNESS: "led_brightness", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", ATTR_FAN_LEVEL: "fan_level", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { # Common set isn't used here. It's a very basic version of the device. - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", ATTR_LED: "led", ATTR_BUZZER: "buzzer", ATTR_CHILD_LOCK: "child_lock", - ATTR_ILLUMINANCE: "illuminance", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", - ATTR_MOTOR_SPEED: "motor_speed", - # perhaps supported but unconfirmed - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", ATTR_VOLUME: "volume", - ATTR_MOTOR2_SPEED: "motor2_speed", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_LEARN_MODE: "learn_mode", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", @@ -255,20 +179,11 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { } AVAILABLE_ATTRIBUTES_AIRFRESH = { - ATTR_TEMPERATURE: "temperature", - ATTR_AIR_QUALITY_INDEX: "aqi", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", - ATTR_CO2: "co2", - ATTR_HUMIDITY: "humidity", ATTR_MODE: "mode", ATTR_LED: "led", - ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_BUZZER: "buzzer", ATTR_CHILD_LOCK: "child_lock", - ATTR_FILTER_LIFE: "filter_life_remaining", - ATTR_FILTER_HOURS_USED: "filter_hours_used", ATTR_USE_TIME: "use_time", - ATTR_MOTOR_SPEED: "motor_speed", ATTR_EXTRA_FEATURES: "extra_features", } @@ -306,7 +221,6 @@ FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_LEARN_MODE | FEATURE_RESET_FILTER @@ -341,7 +255,6 @@ FEATURE_FLAGS_AIRPURIFIER_3 = ( | FEATURE_SET_LED | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_FAN_LEVEL - | FEATURE_SET_LED_BRIGHTNESS ) FEATURE_FLAGS_AIRPURIFIER_V3 = ( @@ -352,17 +265,12 @@ FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_RESET_FILTER | FEATURE_SET_EXTRA_FEATURES ) AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=2))} -) - SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend( {vol.Required(ATTR_LEVEL): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=17))} ) @@ -391,10 +299,6 @@ SERVICE_TO_METHOD = { SERVICE_SET_LEARN_MODE_ON: {"method": "async_set_learn_mode_on"}, SERVICE_SET_LEARN_MODE_OFF: {"method": "async_set_learn_mode_off"}, SERVICE_RESET_FILTER: {"method": "async_reset_filter"}, - SERVICE_SET_LED_BRIGHTNESS: { - "method": "async_set_led_brightness", - "schema": SERVICE_SCHEMA_LED_BRIGHTNESS, - }, SERVICE_SET_FAVORITE_LEVEL: { "method": "async_set_favorite_level", "schema": SERVICE_SCHEMA_FAVORITE_LEVEL, @@ -430,111 +334,98 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fan from a config entry.""" entities = [] - if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + return - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] - name = config_entry.title - model = config_entry.data[CONF_MODEL] - unique_id = config_entry.unique_id + hass.data.setdefault(DATA_KEY, {}) - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - if model in MODELS_PURIFIER_MIOT: - air_purifier = AirPurifierMiot(host, token) - entity = XiaomiAirPurifierMiot( - name, air_purifier, config_entry, unique_id, allowed_failures=2 - ) - elif model.startswith("zhimi.airpurifier."): - air_purifier = AirPurifier(host, token) - entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) - elif model.startswith("zhimi.airfresh."): - air_fresh = AirFresh(host, token) - entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id) + if model in MODELS_PURIFIER_MIOT: + entity = XiaomiAirPurifierMiot( + name, + device, + config_entry, + unique_id, + coordinator, + ) + elif model.startswith("zhimi.airpurifier."): + entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) + elif model.startswith("zhimi.airfresh."): + entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + else: + return + + hass.data[DATA_KEY][unique_id] = entity + + entities.append(entity) + + async def async_service_handler(service): + """Map services to methods on XiaomiAirPurifier.""" + method = SERVICE_TO_METHOD[service.service] + params = { + key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID + } + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + filtered_entities = [ + entity + for entity in hass.data[DATA_KEY].values() + if entity.entity_id in entity_ids + ] else: - _LOGGER.error( - "Unsupported device found! Please create an issue at " - "https://github.com/syssi/xiaomi_airpurifier/issues " - "and provide the following data: %s", - model, - ) - return + filtered_entities = hass.data[DATA_KEY].values() - hass.data[DATA_KEY][host] = entity - entities.append(entity) + update_tasks = [] - async def async_service_handler(service): - """Map services to methods on XiaomiAirPurifier.""" - method = SERVICE_TO_METHOD[service.service] - params = { - key: value - for key, value in service.data.items() - if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - entities = [ - entity - for entity in hass.data[DATA_KEY].values() - if entity.entity_id in entity_ids - ] - else: - entities = hass.data[DATA_KEY].values() - - update_tasks = [] - - for entity in entities: - entity_method = getattr(entity, method["method"], None) - if not entity_method: - continue - await entity_method(**params) - update_tasks.append( - hass.async_create_task(entity.async_update_ha_state(True)) - ) - - if update_tasks: - await asyncio.wait(update_tasks) - - for air_purifier_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, schema=schema + for entity in filtered_entities: + entity_method = getattr(entity, method["method"], None) + if not entity_method: + continue + await entity_method(**params) + update_tasks.append( + hass.async_create_task(entity.async_update_ha_state(True)) ) - async_add_entities(entities, update_before_add=True) + if update_tasks: + await asyncio.wait(update_tasks) + + for air_purifier_service, method in SERVICE_TO_METHOD.items(): + schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, air_purifier_service, async_service_handler, schema=schema + ) + + async_add_entities(entities) -class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): +class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id) + super().__init__(name, device, entry, unique_id, coordinator) self._available = False + self._available_attributes = {} self._state = None + self._mode = None + self._fan_level = None self._state_attrs = {ATTR_MODEL: self._model} self._device_features = FEATURE_SET_CHILD_LOCK - self._skip_update = False self._supported_features = 0 self._speed_count = 100 self._preset_modes = [] - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [] @property def supported_features(self): """Flag supported features.""" return self._supported_features - # the speed_list attribute is deprecated, support will end with release 2021.7 - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - @property def speed_count(self): """Return the number of speeds of the fan supported.""" @@ -583,22 +474,20 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): return value - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a miio device command handling error messages.""" - try: - result = await self.hass.async_add_executor_job( - partial(func, *args, **kwargs) - ) - - _LOGGER.debug("Response received from miio device: %s", result) - - return result == SUCCESS - except DeviceException as exc: - if self._available: - _LOGGER.error(mask_error, exc) - self._available = False - - return False + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._available = True + self._state = self.coordinator.data.is_on + self._state_attrs.update( + { + key: self._extract_value_from_attribute(self.coordinator.data, value) + for key, value in self._available_attributes.items() + } + ) + self._mode = self._state_attrs.get(ATTR_MODE) + self._fan_level = self._state_attrs.get(ATTR_FAN_LEVEL) + self.async_write_ha_state() # # The fan entity model has changed to use percentages and preset_modes @@ -615,22 +504,19 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): **kwargs, ) -> None: """Turn the 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) + result = await self._try_command( + "Turning the miio device on failed.", self._device.on + ) + # If operation mode was set the device must not be turned on. if percentage: 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 - self._skip_update = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" @@ -640,7 +526,7 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): if result: self._state = False - self._skip_update = True + self.async_write_ha_state() async def async_set_buzzer_on(self): """Turn the buzzer on.""" @@ -706,113 +592,52 @@ class XiaomiAirPurifier(XiaomiGenericDevice): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, name, device, entry, unique_id, allowed_failures=0): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id) - self._allowed_failures = allowed_failures - self._failure = 0 + super().__init__(name, device, entry, unique_id, coordinator) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7 self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_2S self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_2S elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_3 self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE self._speed_count = 3 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_3 elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_V3 self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 else: self._device_features = FEATURE_FLAGS_AIRPURIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER self._preset_modes = PRESET_MODES_AIRPURIFIER self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [] self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) - - async def async_update(self): - """Fetch state from the device.""" - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - { - key: self._extract_value_from_attribute(state, value) - for key, value in self._available_attributes.items() - } - ) - - self._failure = 0 - - except DeviceException as ex: - self._failure += 1 - if self._failure < self._allowed_failures: - _LOGGER.info( - "Got exception while fetching the state: %s, failure: %d", - ex, - self._failure, - ) - else: - if self._available: - self._available = False - _LOGGER.error( - "Got exception while fetching the state: %s, failure: %d", - ex, - self._failure, - ) + self._mode = self._state_attrs.get(ATTR_MODE) + self._fan_level = self._state_attrs.get(ATTR_FAN_LEVEL) @property def preset_mode(self): @@ -835,15 +660,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. @@ -873,21 +689,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self.PRESET_MODE_MAPPING[preset_mode], ) - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirpurifierOperationMode[speed.title()], - ) - async def async_set_led_on(self): """Turn the led on.""" if self._device_features & FEATURE_SET_LED == 0: @@ -908,17 +709,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): False, ) - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierLedBrightness(brightness), - ) - async def async_set_favorite_level(self, level: int = 1): """Set the favorite level.""" if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: @@ -1032,8 +822,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): def percentage(self): """Return the current percentage based speed.""" if self._state: - fan_level = self._state_attrs[ATTR_FAN_LEVEL] - return ranged_value_to_percentage((1, 3), fan_level) + return ranged_value_to_percentage((1, 3), self._fan_level) return None @@ -1041,34 +830,26 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): def preset_mode(self): """Get the active preset mode.""" if self._state: - preset_mode = AirpurifierMiotOperationMode( - self._state_attrs[ATTR_MODE] - ).name + preset_mode = AirpurifierMiotOperationMode(self._mode).name return preset_mode if preset_mode in self._preset_modes else None return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirpurifierMiotOperationMode(self._state_attrs[ATTR_MODE]).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. This method is a coroutine. """ fan_level = math.ceil(percentage_to_ranged_value((1, 3), percentage)) - if fan_level: - await self._try_command( - "Setting fan level of the miio device failed.", - self._device.set_fan_level, - fan_level, - ) + if not fan_level: + return + if await self._try_command( + "Setting fan level of the miio device failed.", + self._device.set_fan_level, + fan_level, + ): + self._fan_level = fan_level + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan. @@ -1078,37 +859,13 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, self.PRESET_MODE_MAPPING[preset_mode], - ) - - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirpurifierMiotOperationMode[speed.title()], - ) - - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierMiotLedBrightness(brightness), - ) + ): + self._mode = self.PRESET_MODE_MAPPING[preset_mode].value + self.async_write_ha_state() class XiaomiAirFresh(XiaomiGenericDevice): @@ -1128,50 +885,25 @@ class XiaomiAirFresh(XiaomiGenericDevice): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id) + super().__init__(name, device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH - # the speed_list attribute is deprecated, support will end with release 2021.7 - 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} ) - - async def async_update(self): - """Fetch state from the device.""" - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - { - key: self._extract_value_from_attribute(state, value) - for key, value in self._available_attributes.items() - } - ) - - except DeviceException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) + self._mode = self._state_attrs.get(ATTR_MODE) @property def preset_mode(self): """Get the active preset mode.""" if self._state: - preset_mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name + preset_mode = AirfreshOperationMode(self._mode).name return preset_mode if preset_mode in self._preset_modes else None return None @@ -1180,7 +912,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): def percentage(self): """Return the current percentage based speed.""" if self._state: - mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]) + mode = AirfreshOperationMode(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] @@ -1188,15 +920,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. @@ -1206,11 +929,15 @@ class XiaomiAirFresh(XiaomiGenericDevice): percentage_to_ranged_value((1, self._speed_count), percentage) ) if speed_mode: - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, AirfreshOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), - ) + ): + self._mode = AirfreshOperationMode( + self.SPEED_MODE_MAPPING[speed_mode] + ).value + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan. @@ -1220,26 +947,13 @@ class XiaomiAirFresh(XiaomiGenericDevice): if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, self.PRESET_MODE_MAPPING[preset_mode], - ) - - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirfreshOperationMode[speed.title()], - ) + ): + self._mode = self.PRESET_MODE_MAPPING[preset_mode].value + self.async_write_ha_state() async def async_set_led_on(self): """Turn the led on.""" @@ -1261,17 +975,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): False, ) - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirfreshLedBrightness(brightness), - ) - async def async_set_extra_features(self, features: int = 1): """Set the extra features.""" if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 17f42f4bffa..8b7a5c77a17 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -117,7 +117,7 @@ class ConnectXiaomiGateway: miio_cloud = MiCloud(self._cloud_username, self._cloud_password) if not miio_cloud.login(): raise ConfigEntryAuthFailed( - "Could not login to Xioami Miio Cloud, check the credentials" + "Could not login to Xiaomi Miio Cloud, check the credentials" ) devices_raw = miio_cloud.get_devices(self._cloud_country) self._gateway_device.get_devices_from_dict(devices_raw) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 1f37d624b95..6d3c5e50be8 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.6"], + "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.7"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 63fa4e069bf..9cb57e5d3d8 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -4,8 +4,11 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum +from miio.airfresh import LedBrightness as AirfreshLedBrightness from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness +from miio.airpurifier import LedBrightness as AirpurifierLedBrightness +from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import callback @@ -18,8 +21,12 @@ from .const import ( FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_VA2, + MODEL_AIRPURIFIER_M1, + MODEL_AIRPURIFIER_M2, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, + MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -27,10 +34,10 @@ ATTR_LED_BRIGHTNESS = "led_brightness" LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2} -LED_BRIGHTNESS_MAP_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} +LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()} -LED_BRIGHTNESS_REVERSE_MAP_MIOT = { - val: key for key, val in LED_BRIGHTNESS_MAP_MIOT.items() +LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT = { + val: key for key, val in LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT.items() } @@ -65,6 +72,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_class = XiaomiAirHumidifierSelector elif model in MODELS_HUMIDIFIER_MIOT: entity_class = XiaomiAirHumidifierMiotSelector + elif model in [MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2]: + entity_class = XiaomiAirPurifierSelector + elif model in MODELS_PURIFIER_MIOT: + entity_class = XiaomiAirPurifierMiotSelector + elif model == MODEL_AIRFRESH_VA2: + entity_class = XiaomiAirFreshSelector else: return @@ -150,14 +163,62 @@ class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector): @property def led_brightness(self): """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP_MIOT.get(self._current_led_brightness) + return LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT.get( + self._current_led_brightness + ) - async def async_set_led_brightness(self, brightness: str): + async def async_set_led_brightness(self, brightness: str) -> None: """Set the led brightness.""" if await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - AirhumidifierMiotLedBrightness(LED_BRIGHTNESS_MAP_MIOT[brightness]), + AirhumidifierMiotLedBrightness( + LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[brightness] + ), ): - self._current_led_brightness = LED_BRIGHTNESS_MAP_MIOT[brightness] + self._current_led_brightness = LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[ + brightness + ] + self.async_write_ha_state() + + +class XiaomiAirPurifierSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Purifier (MIIO protocol) selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirpurifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self.async_write_ha_state() + + +class XiaomiAirPurifierMiotSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Purifier (MiOT protocol) selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirpurifierMiotLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self.async_write_ha_state() + + +class XiaomiAirFreshSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Fresh selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirfreshLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 852adfcc071..a8a2787aaed 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -19,6 +19,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -26,18 +27,25 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, CONF_HOST, CONF_NAME, CONF_TOKEN, + DEVICE_CLASS_CO2, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, + TIME_HOURS, + VOLUME_CUBIC_METERS, ) import homeassistant.helpers.config_validation as cv @@ -49,9 +57,18 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_VA2, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRPURIFIER_PRO, + MODEL_AIRPURIFIER_PRO_V7, + MODEL_AIRPURIFIER_V2, + MODEL_AIRPURIFIER_V3, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, + MODELS_PURIFIER_MIIO, + MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice @@ -69,18 +86,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" +ATTR_ACTUAL_SPEED = "actual_speed" ATTR_AIR_QUALITY = "air_quality" +ATTR_AQI = "aqi" +ATTR_CARBON_DIOXIDE = "co2" ATTR_CHARGING = "charging" ATTR_DISPLAY_CLOCK = "display_clock" +ATTR_FILTER_LIFE_REMAINING = "filter_life_remaining" +ATTR_FILTER_HOURS_USED = "filter_hours_used" +ATTR_FILTER_USE = "filter_use" ATTR_HUMIDITY = "humidity" ATTR_ILLUMINANCE = "illuminance" +ATTR_ILLUMINANCE_LUX = "illuminance_lux" ATTR_LOAD_POWER = "load_power" +ATTR_MOTOR2_SPEED = "motor2_speed" +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_PM25 = "pm25" ATTR_POWER = "power" ATTR_PRESSURE = "pressure" +ATTR_PURIFY_VOLUME = "purify_volume" ATTR_SENSOR_STATE = "sensor_state" ATTR_WATER_LEVEL = "water_level" @@ -89,86 +116,213 @@ ATTR_WATER_LEVEL = "water_level" class XiaomiMiioSensorDescription(SensorEntityDescription): """Class that holds device specific info for a xiaomi aqara or humidifier sensor.""" - valid_min_value: float | None = None - valid_max_value: float | None = None + attributes: tuple = () SENSOR_TYPES = { ATTR_TEMPERATURE: XiaomiMiioSensorDescription( key=ATTR_TEMPERATURE, name="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_HUMIDITY: XiaomiMiioSensorDescription( key=ATTR_HUMIDITY, name="Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_PRESSURE: XiaomiMiioSensorDescription( key=ATTR_PRESSURE, name="Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, name="Load Power", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, name="Water Level", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=STATE_CLASS_MEASUREMENT, - valid_min_value=0.0, - valid_max_value=100.0, ), - ATTR_ACTUAL_MOTOR_SPEED: XiaomiMiioSensorDescription( - key=ATTR_ACTUAL_MOTOR_SPEED, + ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( + key=ATTR_ACTUAL_SPEED, name="Actual Speed", - unit_of_measurement="rpm", + native_unit_of_measurement="rpm", + icon="mdi:fast-forward", + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( + key=ATTR_MOTOR_SPEED, + name="Motor Speed", + native_unit_of_measurement="rpm", + icon="mdi:fast-forward", + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( + key=ATTR_MOTOR2_SPEED, + name="Second Motor Speed", + native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, - valid_min_value=200.0, - valid_max_value=2000.0, ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", - unit_of_measurement=UNIT_LUMEN, + native_unit_of_measurement=UNIT_LUMEN, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_ILLUMINANCE_LUX: XiaomiMiioSensorDescription( + key=ATTR_ILLUMINANCE, + name="Illuminance", + native_unit_of_measurement=LIGHT_LUX, device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_AIR_QUALITY: XiaomiMiioSensorDescription( key=ATTR_AIR_QUALITY, - unit_of_measurement="AQI", + native_unit_of_measurement="AQI", icon="mdi:cloud", state_class=STATE_CLASS_MEASUREMENT, ), + ATTR_PM25: XiaomiMiioSensorDescription( + key=ATTR_AQI, + name="PM2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( + key=ATTR_FILTER_LIFE_REMAINING, + name="Filter Life Remaining", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:air-filter", + state_class=STATE_CLASS_MEASUREMENT, + attributes=("filter_type",), + ), + ATTR_FILTER_USE: XiaomiMiioSensorDescription( + key=ATTR_FILTER_HOURS_USED, + name="Filter Use", + native_unit_of_measurement=TIME_HOURS, + icon="mdi:clock-outline", + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( + key=ATTR_CARBON_DIOXIDE, + name="Carbon Dioxide", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_PURIFY_VOLUME: XiaomiMiioSensorDescription( + key=ATTR_PURIFY_VOLUME, + name="Purify Volume", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), } -HUMIDIFIER_MIIO_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", -} +HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_WATER_LEVEL) +HUMIDIFIER_CA1_CB1_SENSORS = ( + ATTR_HUMIDITY, + ATTR_TEMPERATURE, + ATTR_MOTOR_SPEED, + ATTR_WATER_LEVEL, +) +HUMIDIFIER_MIOT_SENSORS = ( + ATTR_ACTUAL_SPEED, + ATTR_HUMIDITY, + ATTR_TEMPERATURE, + ATTR_WATER_LEVEL, +) +HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) -HUMIDIFIER_MIOT_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", - ATTR_WATER_LEVEL: "water_level", - ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", -} +PURIFIER_MIIO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TEMPERATURE, +) +PURIFIER_MIOT_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_V2_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_V3_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, +) +PURIFIER_PRO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_PRO_V7_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TEMPERATURE, +) +AIRFRESH_SENSORS = ( + ATTR_CARBON_DIOXIDE, + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_PM25, + ATTR_TEMPERATURE, +) -HUMIDIFIER_MJJSQ_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", +MODEL_TO_SENSORS_MAP = { + MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, + MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, + MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, + MODEL_AIRPURIFIER_V3: PURIFIER_V3_SENSORS, + MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, + MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, + MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, } @@ -223,17 +377,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] model = config_entry.data[CONF_MODEL] - device = None + device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) sensors = [] - if model in MODELS_HUMIDIFIER_MIOT: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + if model in MODEL_TO_SENSORS_MAP: + sensors = MODEL_TO_SENSORS_MAP[model] + elif model in MODELS_HUMIDIFIER_MIOT: sensors = HUMIDIFIER_MIOT_SENSORS elif model in MODELS_HUMIDIFIER_MJJSQ: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MJJSQ_SENSORS elif model in MODELS_HUMIDIFIER_MIIO: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MIIO_SENSORS + elif model in MODELS_PURIFIER_MIIO: + sensors = PURIFIER_MIIO_SENSORS + elif model in MODELS_PURIFIER_MIOT: + sensors = PURIFIER_MIOT_SENSORS else: unique_id = config_entry.unique_id name = config_entry.title @@ -272,24 +429,23 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): self._attr_name = name self._attr_unique_id = unique_id - self._state = None self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the device.""" - self._state = self._extract_value_from_attribute( + return self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) - if ( - self.entity_description.valid_min_value - and self._state < self.entity_description.valid_min_value - ) or ( - self.entity_description.valid_max_value - and self._state > self.entity_description.valid_max_value - ): - return None - return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + attr: self._extract_value_from_attribute(self.coordinator.data, attr) + for attr in self.entity_description.attributes + if hasattr(self.coordinator.data, attr) + } @staticmethod def _extract_value_from_attribute(state, attribute): @@ -327,7 +483,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -374,7 +530,7 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._sub_device.status[self.entity_description.key] @@ -399,7 +555,7 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index 4c153292d7e..43300f8381a 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -101,24 +101,6 @@ fan_set_fan_level: min: 1 max: 3 -fan_set_led_brightness: - name: Fan set LED brightness - description: Set the led brightness. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - required: true - selector: - number: - min: 0 - max: 2 - fan_set_auto_detect_on: name: Fan set auto detect on description: Turn the auto detect on. diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 69a1621c973..129f6f1ecbf 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -12,7 +12,7 @@ "unknown_device": "The device model is not known, not able to setup the device using config flow.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", - "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials." + "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index ff0a24170f6..7e9d7d5c7eb 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds", - "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xioami Miio Cloud, comprova les credencials.", + "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xiaomi Miio Cloud, comprova les credencials.", "cloud_no_devices": "No s'han trobat dispositius en aquest compte al n\u00favol de Xiaomi Miio.", "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 17363b347c0..24e639e3a23 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben", - "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.", + "cloud_login_error": "Die Anmeldung bei Xiaomi Miio Cloud ist fehlgeschlagen, \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." diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index cbe10230093..84593a3edc1 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Failed to connect", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", - "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials.", + "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index 92d8ffe048f..4eb326d7f08 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "\u00dchendus nurjus", "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik", - "cloud_login_error": "Xioami Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", + "cloud_login_error": "Xiaomi Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", "cloud_no_devices": "Xiaomi Miio pilvekontolt ei leitud \u00fchtegi seadet.", "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 8fa93169647..a296dd7aa08 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land", - "cloud_login_error": "Kunne ikke logge p\u00e5 Xioami Miio Cloud, sjekk legitimasjonen.", + "cloud_login_error": "Kunne ikke logge inn p\u00e5 Xiaomi Miio Cloud, sjekk legitimasjonen.", "cloud_no_devices": "Ingen enheter funnet i denne Xiaomi Miio-skykontoen.", "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index f9aeb824b20..017660e51c6 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443.", - "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xioami Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xiaomi Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "cloud_no_devices": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json new file mode 100644 index 00000000000..bc96de04645 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillant", + "dim": "Atenua", + "off": "OFF" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.cs.json b/homeassistant/components/xiaomi_miio/translations/select.cs.json new file mode 100644 index 00000000000..d7f5e8b6c84 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "off": "Vypnuto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.es.json b/homeassistant/components/xiaomi_miio/translations/select.es.json new file mode 100644 index 00000000000..3906ef91342 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillo", + "dim": "Atenuar", + "off": "Apagado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.he.json b/homeassistant/components/xiaomi_miio/translations/select.he.json index 0059da60e86..2cffbc3b457 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.he.json +++ b/homeassistant/components/xiaomi_miio/translations/select.he.json @@ -1,6 +1,8 @@ { "state": { "xiaomi_miio__led_brightness": { + "bright": "\u05d1\u05d4\u05d9\u05e8", + "dim": "\u05de\u05e2\u05d5\u05de\u05e2\u05dd", "off": "\u05db\u05d1\u05d5\u05d9" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.hu.json b/homeassistant/components/xiaomi_miio/translations/select.hu.json new file mode 100644 index 00000000000..4e6df2b4a33 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "F\u00e9nyes", + "dim": "Hom\u00e1lyos", + "off": "Ki" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json new file mode 100644 index 00000000000..21e79e41e99 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillante", + "dim": "Fioca", + "off": "Spento" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.no.json b/homeassistant/components/xiaomi_miio/translations/select.no.json new file mode 100644 index 00000000000..8205447ac2c --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Lys", + "dim": "Dim", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ru.json b/homeassistant/components/xiaomi_miio/translations/select.ru.json index 4dac3002d1b..138d2b4fdce 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ru.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ru.json @@ -3,7 +3,7 @@ "xiaomi_miio__led_brightness": { "bright": "\u042f\u0440\u043a\u043e", "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", - "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "off": "\u041e\u0442\u043a\u043b." } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json new file mode 100644 index 00000000000..bad6ba91597 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "\u4eae", + "dim": "\u6697", + "off": "\u5173" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index 0034f73fbf3..c3eb4affc4c 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -56,7 +56,8 @@ "data": { "host": "IP \u5730\u5740", "token": "API Token" - } + }, + "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\uff0c\u8bf7\u53c2\u8003 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4e2d\u63d0\u5230\u7684\u65b9\u6cd5\u83b7\u53d6\u8be5\u4fe1\u606f\u3002\u8bf7\u6ce8\u610f\uff0c\u8be5 API Token \u4e0d\u540c\u4e8e \"Xiaomi Aqara\" \u96c6\u6210\u6240\u4f7f\u7528\u7684\u5bc6\u94a5\u3002" }, "reauth_confirm": { "description": "\u5c0f\u7c73 Miio \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237\uff0c\u4ee5\u4fbf\u66f4\u65b0 token \u6216\u6dfb\u52a0\u4e22\u5931\u7684\u4e91\u7aef\u51ed\u636e\u3002" diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index f158e7d74b8..ed022f5b9e7 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -37,11 +37,11 @@ class XS1Sensor(XS1DeviceEntity, SensorEntity): return self.device.name() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.device.value() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.device.unit() diff --git a/homeassistant/components/yale_smart_alarm/translations/es-419.json b/homeassistant/components/yale_smart_alarm/translations/es-419.json new file mode 100644 index 00000000000..f3cbae5ed03 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/es-419.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de \u00c1rea" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json new file mode 100644 index 00000000000..b970badb079 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de \u00c1rea", + "name": "Nombre", + "password": "Clave", + "username": "Nombre de usuario" + } + }, + "user": { + "data": { + "area_id": "ID de \u00e1rea", + "name": "Nombre", + "password": "Clave", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/hu.json b/homeassistant/components/yale_smart_alarm/translations/hu.json new file mode 100644 index 00000000000..8c60574227d --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Ter\u00fclet ID", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "area_id": "Ter\u00fclet ID", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ 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 index bbeedb7dc89..eba8861fa46 100644 --- a/homeassistant/components/yale_smart_alarm/translations/no.json +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, "step": { "reauth_confirm": { "data": { + "area_id": "Omr\u00e5de -ID", "name": "Navn", "password": "Passord", "username": "Brukernavn" @@ -10,6 +17,7 @@ }, "user": { "data": { + "area_id": "Omr\u00e5de -ID", "name": "Navn", "password": "Passord", "username": "Brukernavn" diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json new file mode 100644 index 00000000000..2afd7efdb15 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "\u533a\u57df ID", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, + "user": { + "data": { + "area_id": "\u533a\u57df ID", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "username": "\u8d26\u53f7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json new file mode 100644 index 00000000000..f69ab4546d5 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u8bbe\u7f6e MusicCast \u4ee5\u4e0e Home Assistant \u96c6\u6210\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 08e856a721e..b4f7f986626 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -108,7 +108,7 @@ class DiscoverYandexTransport(SensorEntity): self._attrs = attrs @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 2cb754ce6a7..0ea4eb8e84f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -2,25 +2,34 @@ from __future__ import annotations import asyncio +import contextlib from datetime import timedelta import logging +from urllib.parse import urlparse +from async_upnp_client.search import SSDPListener import voluptuous as vol -from yeelight import Bulb, BulbException, discover_bulbs +from yeelight import BulbException +from yeelight.aio import KEY_CONNECTED, AsyncBulb +from homeassistant import config_entries from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_ID, CONF_NAME, - CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -46,7 +55,6 @@ CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" -DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" DATA_PLATFORMS_LOADED = "platforms_loaded" @@ -65,8 +73,13 @@ ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" -SCAN_INTERVAL = timedelta(seconds=30) DISCOVERY_INTERVAL = timedelta(seconds=60) +SSDP_TARGET = ("239.255.255.250", 1982) +SSDP_ST = "wifi_bulb" +DISCOVERY_ATTEMPTS = 3 +DISCOVERY_SEARCH_INTERVAL = timedelta(seconds=2) +DISCOVERY_TIMEOUT = 2 + YEELIGHT_RGB_TRANSITION = "RGBTransition" YEELIGHT_HSV_TRANSACTION = "HSVTransition" @@ -114,7 +127,6 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_CUSTOM_EFFECTS): [ { vol.Required(CONF_NAME): cv.string, @@ -152,13 +164,12 @@ UPDATE_REQUEST_PROPERTIES = [ PLATFORMS = ["binary_sensor", "light"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Yeelight bulbs.""" conf = config.get(DOMAIN, {}) hass.data[DOMAIN] = { DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CONFIG_ENTRIES: {}, - DATA_SCAN_INTERVAL: conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), } # Import manually configured devices @@ -193,7 +204,10 @@ async def _async_initialize( hass.config_entries.async_setup_platforms(entry, PLATFORMS) if not device: + # get device and start listening for local pushes device = await _async_get_device(hass, host, entry) + + await device.async_setup() entry_data[DATA_DEVICE] = device entry.async_on_unload( @@ -202,8 +216,8 @@ async def _async_initialize( ) ) - entry.async_on_unload(device.async_unload) - await device.async_setup() + # fetch initial state + asyncio.create_task(device.async_update()) @callback @@ -240,7 +254,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.data.get(CONF_HOST): try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) - except OSError as ex: + except BulbException as ex: # If CONF_ID is not valid we cannot fallback to discovery # so we must retry by raising ConfigEntryNotReady if not entry.data.get(CONF_ID): @@ -248,16 +262,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Otherwise fall through to discovery else: # manually added device - await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) + try: + await _async_initialize( + hass, entry, entry.data[CONF_HOST], device=device + ) + except BulbException as ex: + raise ConfigEntryNotReady from ex return True + async def _async_from_discovery(capabilities: dict[str, str]) -> None: + host = urlparse(capabilities["location"]).hostname + try: + await _async_initialize(hass, entry, host) + except BulbException: + _LOGGER.exception("Failed to connect to bulb at %s", host) + # discovery scanner = YeelightScanner.async_get(hass) - - async def _async_from_discovery(host: str) -> None: - await _async_initialize(hass, entry, host) - - scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) + await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -275,6 +297,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = YeelightScanner.async_get(hass) scanner.async_unregister_callback(entry.data[CONF_ID]) + if DATA_DEVICE in entry_data: + device = entry_data[DATA_DEVICE] + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + _LOGGER.debug("Yeelight Listener stopped") + data_config_entries.pop(entry.entry_id) return True @@ -283,9 +311,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" - model = capabilities["model"] - unique_id = capabilities["id"] - return f"yeelight_{model}_{unique_id}" + model = str(capabilities["model"]).replace("_", " ").title() + short_id = hex(int(capabilities["id"], 16)) + return f"Yeelight {model} {short_id}" async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): @@ -309,89 +337,147 @@ class YeelightScanner: def __init__(self, hass: HomeAssistant) -> None: """Initialize class.""" self._hass = hass - self._seen = {} self._callbacks = {} - self._scan_task = None + self._host_discovered_events = {} + self._unique_id_capabilities = {} + self._host_capabilities = {} + self._track_interval = None + self._listener = None + self._connected_event = None - async def _async_scan(self): - _LOGGER.debug("Yeelight scanning") - # Run 3 times as packets can get lost - for _ in range(3): - devices = await self._hass.async_add_executor_job(discover_bulbs) - for device in devices: - unique_id = device["capabilities"]["id"] - if unique_id in self._seen: - continue - host = device["ip"] - self._seen[unique_id] = host - _LOGGER.debug("Yeelight discovered at %s", host) - if unique_id in self._callbacks: - self._hass.async_create_task(self._callbacks[unique_id](host)) - self._callbacks.pop(unique_id) - if len(self._callbacks) == 0: - self._async_stop_scan() + async def async_setup(self): + """Set up the scanner.""" + if self._connected_event: + await self._connected_event.wait() + return + self._connected_event = asyncio.Event() - await asyncio.sleep(SCAN_INTERVAL.total_seconds()) - self._scan_task = self._hass.loop.create_task(self._async_scan()) + async def _async_connected(): + self._listener.async_search() + self._connected_event.set() + + self._listener = SSDPListener( + async_callback=self._async_process_entry, + service_type=SSDP_ST, + target=SSDP_TARGET, + async_connect_callback=_async_connected, + ) + await self._listener.async_start() + await self._connected_event.wait() + + async def async_discover(self): + """Discover bulbs.""" + await self.async_setup() + for _ in range(DISCOVERY_ATTEMPTS): + self._listener.async_search() + await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds()) + return self._unique_id_capabilities.values() @callback - def _async_start_scan(self): + def async_scan(self, *_): + """Send discovery packets.""" + _LOGGER.debug("Yeelight scanning") + self._listener.async_search() + + async def async_get_capabilities(self, host): + """Get capabilities via SSDP.""" + if host in self._host_capabilities: + return self._host_capabilities[host] + + host_event = asyncio.Event() + self._host_discovered_events.setdefault(host, []).append(host_event) + await self.async_setup() + + self._listener.async_search((host, SSDP_TARGET[1])) + + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(host_event.wait(), timeout=DISCOVERY_TIMEOUT) + + self._host_discovered_events[host].remove(host_event) + return self._host_capabilities.get(host) + + def _async_discovered_by_ssdp(self, response): + @callback + def _async_start_flow(*_): + asyncio.create_task( + self._hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=response, + ) + ) + + # Delay starting the flow in case the discovery is the result + # of another discovery + async_call_later(self._hass, 1, _async_start_flow) + + async def _async_process_entry(self, response): + """Process a discovery.""" + _LOGGER.debug("Discovered via SSDP: %s", response) + unique_id = response["id"] + host = urlparse(response["location"]).hostname + if unique_id not in self._unique_id_capabilities: + _LOGGER.debug("Yeelight discovered with %s", response) + self._async_discovered_by_ssdp(response) + self._host_capabilities[host] = response + self._unique_id_capabilities[unique_id] = response + for event in self._host_discovered_events.get(host, []): + event.set() + if unique_id in self._callbacks: + self._hass.async_create_task(self._callbacks[unique_id](response)) + self._callbacks.pop(unique_id) + if not self._callbacks: + self._async_stop_scan() + + async def _async_start_scan(self): """Start scanning for Yeelight devices.""" _LOGGER.debug("Start scanning") - # Use loop directly to avoid home assistant track this task - self._scan_task = self._hass.loop.create_task(self._async_scan()) + await self.async_setup() + if not self._track_interval: + self._track_interval = async_track_time_interval( + self._hass, self.async_scan, DISCOVERY_INTERVAL + ) + self.async_scan() @callback def _async_stop_scan(self): """Stop scanning.""" - _LOGGER.debug("Stop scanning") - if self._scan_task is not None: - self._scan_task.cancel() - self._scan_task = None + if self._track_interval is None: + return + _LOGGER.debug("Stop scanning interval") + self._track_interval() + self._track_interval = None - @callback - def async_register_callback(self, unique_id, callback_func): + async def async_register_callback(self, unique_id, callback_func): """Register callback function.""" - host = self._seen.get(unique_id) - if host is not None: - self._hass.async_create_task(callback_func(host)) - else: - self._callbacks[unique_id] = callback_func - if len(self._callbacks) == 1: - self._async_start_scan() + if capabilities := self._unique_id_capabilities.get(unique_id): + self._hass.async_create_task(callback_func(capabilities)) + return + self._callbacks[unique_id] = callback_func + await self._async_start_scan() @callback def async_unregister_callback(self, unique_id): """Unregister callback function.""" - if unique_id not in self._callbacks: - return - self._callbacks.pop(unique_id) - if len(self._callbacks) == 0: + self._callbacks.pop(unique_id, None) + if not self._callbacks: self._async_stop_scan() class YeelightDevice: """Represents single Yeelight device.""" - def __init__(self, hass, host, config, bulb, capabilities): + def __init__(self, hass, host, config, bulb): """Initialize device.""" self._hass = hass self._config = config self._host = host self._bulb_device = bulb - self._capabilities = capabilities or {} + self._capabilities = {} self._device_type = None self._available = False - self._remove_time_tracker = None self._initialized = False - - self._name = host # Default name is host - if capabilities: - # Generate name from model and id when capabilities is available - self._name = _async_unique_name(capabilities) - if config.get(CONF_NAME): - # Override default name when name is set in config - self._name = config[CONF_NAME] + self._name = None @property def bulb(self): @@ -421,7 +507,7 @@ class YeelightDevice: @property def model(self): """Return configured/autodetected device model.""" - return self._bulb_device.model + return self._bulb_device.model or self._capabilities.get("model") @property def fw_version(self): @@ -478,34 +564,37 @@ class YeelightDevice: return self._device_type - def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): + async def async_turn_on( + self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None + ): """Turn on device.""" try: - self.bulb.turn_on( + await self.bulb.async_turn_on( duration=duration, light_type=light_type, power_mode=power_mode ) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) - def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): + async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" try: - self.bulb.turn_off(duration=duration, light_type=light_type) + await self.bulb.async_turn_off(duration=duration, light_type=light_type) except BulbException as ex: _LOGGER.error( "Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex ) - def _update_properties(self): + async def _async_update_properties(self): """Read new properties from the device.""" if not self.bulb: return try: - self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True if not self._initialized: - self._initialize_device() + self._initialized = True + async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -515,49 +604,32 @@ class YeelightDevice: return self._available - def _get_capabilities(self): - """Request device capabilities.""" - try: - self.bulb.get_capabilities() - _LOGGER.debug( - "Device %s, %s capabilities: %s", - self._host, - self.name, - self.bulb.capabilities, - ) - except BulbException as ex: - _LOGGER.error( - "Unable to get device capabilities %s, %s: %s", - self._host, - self.name, - ex, - ) - - def _initialize_device(self): - self._get_capabilities() - self._initialized = True - dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) - - def update(self): - """Update device properties and send data updated signal.""" - self._update_properties() - dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) - async def async_setup(self): - """Set up the device.""" + """Fetch capabilities and setup name if available.""" + scanner = YeelightScanner.async_get(self._hass) + self._capabilities = await scanner.async_get_capabilities(self._host) or {} + if name := self._config.get(CONF_NAME): + # Override default name when name is set in config + self._name = name + elif self._capabilities: + # Generate name from model and id when capabilities is available + self._name = _async_unique_name(self._capabilities) + else: + self._name = self._host # Default name is host - async def _async_update(_): - await self._hass.async_add_executor_job(self.update) - - await _async_update(None) - self._remove_time_tracker = async_track_time_interval( - self._hass, _async_update, self._hass.data[DOMAIN][DATA_SCAN_INTERVAL] - ) + async def async_update(self): + """Update device properties and send data updated signal.""" + if self._initialized and self._available: + # No need to poll, already connected + return + await self._async_update_properties() + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) @callback - def async_unload(self): - """Unload the device.""" - self._remove_time_tracker() + def async_update_callback(self, data): + """Update push from device.""" + self._available = data.get(KEY_CONNECTED, True) + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) class YeelightEntity(Entity): @@ -597,9 +669,9 @@ class YeelightEntity(Entity): """No polling needed.""" return False - def update(self) -> None: + async def async_update(self) -> None: """Update the entity.""" - self._device.update() + await self._device.async_update() async def _async_get_device( @@ -609,7 +681,20 @@ async def _async_get_device( model = entry.options.get(CONF_MODEL) # Set up device - bulb = Bulb(host, model=model or None) - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + bulb = AsyncBulb(host, model=model or None) - return YeelightDevice(hass, host, entry.options, bulb, capabilities) + device = YeelightDevice(hass, host, entry.options, bulb) + # start listening for local pushes + await device.bulb.async_listen(device.async_update_callback) + + # register stop callback to shutdown listening for local pushes + async def async_stop_listen_task(event): + """Stop listen thread.""" + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) + ) + + return device diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 4fe3709cdd2..185bb504a1b 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -33,6 +33,7 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def unique_id(self) -> str: diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index a66571cae93..d93f59535cf 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Yeelight integration.""" import logging +from urllib.parse import urlparse import voluptuous as vol import yeelight +from yeelight.aio import AsyncBulb from homeassistant import config_entries, exceptions from homeassistant.components.dhcp import IP_ADDRESS @@ -19,6 +21,7 @@ from . import ( CONF_TRANSITION, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, + YeelightScanner, _async_unique_name, ) @@ -54,6 +57,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_ip = discovery_info[IP_ADDRESS] return await self._async_handle_discovery() + async def async_step_ssdp(self, discovery_info): + """Handle discovery from ssdp.""" + self._discovered_ip = urlparse(discovery_info["location"]).hostname + await self.async_set_unique_id(discovery_info["id"]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._discovered_ip}, reload_on_update=False + ) + return await self._async_handle_discovery() + async def _async_handle_discovery(self): """Handle any discovery.""" self.context[CONF_HOST] = self._discovered_ip @@ -62,7 +74,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") try: - self._discovered_model = await self._async_try_connect(self._discovered_ip) + self._discovered_model = await self._async_try_connect( + self._discovered_ip, raise_on_progress=True + ) except CannotConnect: return self.async_abort(reason="cannot_connect") @@ -96,7 +110,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input.get(CONF_HOST): return await self.async_step_pick_device() try: - model = await self._async_try_connect(user_input[CONF_HOST]) + model = await self._async_try_connect( + user_input[CONF_HOST], raise_on_progress=False + ) except CannotConnect: errors["base"] = "cannot_connect" else: @@ -119,10 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: unique_id = user_input[CONF_DEVICE] capabilities = self._discovered_devices[unique_id] - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(unique_id, raise_on_progress=False) self._abort_if_unique_id_configured() + host = urlparse(capabilities["location"]).hostname return self.async_create_entry( - title=_async_unique_name(capabilities), data={CONF_ID: unique_id} + title=_async_unique_name(capabilities), + data={CONF_ID: unique_id, CONF_HOST: host}, ) configured_devices = { @@ -131,19 +149,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_ID] } devices_name = {} + scanner = YeelightScanner.async_get(self.hass) + devices = await scanner.async_discover() # Run 3 times as packets can get lost - for _ in range(3): - devices = await self.hass.async_add_executor_job(yeelight.discover_bulbs) - for device in devices: - capabilities = device["capabilities"] - unique_id = capabilities["id"] - if unique_id in configured_devices: - continue # ignore configured devices - model = capabilities["model"] - host = device["ip"] - name = f"{host} {model} {unique_id}" - self._discovered_devices[unique_id] = capabilities - devices_name[unique_id] = name + for capabilities in devices: + unique_id = capabilities["id"] + if unique_id in configured_devices: + continue # ignore configured devices + model = capabilities["model"] + host = urlparse(capabilities["location"]).hostname + name = f"{host} {model} {unique_id}" + self._discovered_devices[unique_id] = capabilities + devices_name[unique_id] = name # Check if there is at least one device if not devices_name: @@ -157,7 +174,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import step.""" host = user_input[CONF_HOST] try: - await self._async_try_connect(host) + await self._async_try_connect(host, raise_on_progress=False) except CannotConnect: _LOGGER.error("Failed to import %s: cannot connect", host) return self.async_abort(reason="cannot_connect") @@ -169,27 +186,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - async def _async_try_connect(self, host): + async def _async_try_connect(self, host, raise_on_progress=True): """Set up with options.""" self._async_abort_entries_match({CONF_HOST: host}) - bulb = yeelight.Bulb(host) - try: - capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities) - if capabilities is None: # timeout - _LOGGER.debug("Failed to get capabilities from %s: timeout", host) - else: - _LOGGER.debug("Get capabilities: %s", capabilities) - await self.async_set_unique_id(capabilities["id"]) - return capabilities["model"] - except OSError as err: - _LOGGER.debug("Failed to get capabilities from %s: %s", host, err) - # Ignore the error since get_capabilities uses UDP discovery packet - # which does not work in all network environments - + scanner = YeelightScanner.async_get(self.hass) + capabilities = await scanner.async_get_capabilities(host) + if capabilities is None: # timeout + _LOGGER.debug("Failed to get capabilities from %s: timeout", host) + else: + _LOGGER.debug("Get capabilities: %s", capabilities) + await self.async_set_unique_id( + capabilities["id"], raise_on_progress=raise_on_progress + ) + return capabilities["model"] # Fallback to get properties + bulb = AsyncBulb(host) try: - await self.hass.async_add_executor_job(bulb.get_properties) + await bulb.async_listen(lambda _: True) + await bulb.async_get_properties() + await bulb.async_stop_listening() except yeelight.BulbException as err: _LOGGER.error("Failed to get properties from %s: %s", host, err) raise CannotConnect from err diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8d0a3b0ffd4..4766d897909 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,8 +1,8 @@ """Light platform support for yeelight.""" from __future__ import annotations -from functools import partial import logging +import math import voluptuous as vol import yeelight @@ -234,17 +234,17 @@ def _parse_custom_effects(effects_config): return effects -def _cmd(func): +def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" - def _wrap(self, *args, **kwargs): + async def _async_wrap(self, *args, **kwargs): try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except BulbException as ex: _LOGGER.error("Error when calling %s: %s", func, ex) - return _wrap + return _async_wrap async def async_setup_entry( @@ -306,36 +306,27 @@ def _async_setup_services(hass: HomeAssistant): params = {**service_call.data} params.pop(ATTR_ENTITY_ID) params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) - await hass.async_add_executor_job(partial(entity.start_flow, **params)) + await entity.async_start_flow(**params) async def _async_set_color_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.COLOR, - *service_call.data[ATTR_RGB_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.COLOR, + *service_call.data[ATTR_RGB_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_hsv_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.HSV, - *service_call.data[ATTR_HS_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.HSV, + *service_call.data[ATTR_HS_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_temp_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.CT, - service_call.data[ATTR_KELVIN], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.CT, + service_call.data[ATTR_KELVIN], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_flow_scene(entity, service_call): @@ -344,24 +335,19 @@ def _async_setup_services(hass: HomeAssistant): action=Flow.actions[service_call.data[ATTR_ACTION]], transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), ) - await hass.async_add_executor_job( - partial(entity.set_scene, SceneClass.CF, flow) - ) + await entity.async_set_scene(SceneClass.CF, flow) async def _async_set_auto_delay_off_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.AUTO_DELAY_OFF, - service_call.data[ATTR_BRIGHTNESS], - service_call.data[ATTR_MINUTES], - ) + await entity.async_set_scene( + SceneClass.AUTO_DELAY_OFF, + service_call.data[ATTR_BRIGHTNESS], + service_call.data[ATTR_MINUTES], ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode" + SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode" ) platform.async_register_entity_service( SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow @@ -405,8 +391,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self.config = device.config self._color_temp = None - self._hs = None - self._rgb = None self._effect = None model_specs = self._bulb.get_model_specs() @@ -420,19 +404,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._custom_effects = {} - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.host), - self._schedule_immediate_update, + self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def supported_features(self) -> int: @@ -502,16 +483,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def hs_color(self) -> tuple: """Return the color property.""" - return self._hs + hue = self._get_property("hue") + sat = self._get_property("sat") + if hue is None or sat is None: + return None + + return (int(hue), int(sat)) @property def rgb_color(self) -> tuple: """Return the color property.""" - return self._rgb + rgb = self._get_property("rgb") + + if rgb is None: + return None + + rgb = int(rgb) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + return (red, green, blue) @property def effect(self): """Return the current effect.""" + if not self.device.is_color_flow_enabled: + return None return self._effect @property @@ -561,33 +559,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return yeelight device.""" return self._device - def update(self): + async def async_update(self): """Update light properties.""" - self._hs = self._get_hs_from_properties() - self._rgb = self._get_rgb_from_properties() - if not self.device.is_color_flow_enabled: - self._effect = None - - def _get_hs_from_properties(self): - hue = self._get_property("hue") - sat = self._get_property("sat") - if hue is None or sat is None: - return None - - return (int(hue), int(sat)) - - def _get_rgb_from_properties(self): - rgb = self._get_property("rgb") - - if rgb is None: - return None - - rgb = int(rgb) - blue = rgb & 0xFF - green = (rgb >> 8) & 0xFF - red = (rgb >> 16) & 0xFF - - return (red, green, blue) + await self.device.async_update() def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" @@ -599,53 +573,81 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._bulb.stop_music() - self.device.update() - - @_cmd - def set_brightness(self, brightness, duration) -> None: + @_async_cmd + async def async_set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" if brightness: + if math.floor(self.brightness) == math.floor(brightness): + _LOGGER.debug("brightness already set to: %s", brightness) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting brightness: %s", brightness) - self._bulb.set_brightness( + await self._bulb.async_set_brightness( brightness / 255 * 100, duration=duration, light_type=self.light_type ) - @_cmd - def set_hs(self, hs_color, duration) -> None: + @_async_cmd + async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if hs_color and COLOR_MODE_HS in self.supported_color_modes: + if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color: + _LOGGER.debug("HS already set to: %s", hs_color) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting HS: %s", hs_color) - self._bulb.set_hsv( + await self._bulb.async_set_hsv( hs_color[0], hs_color[1], duration=duration, light_type=self.light_type ) - @_cmd - def set_rgb(self, rgb, duration) -> None: + @_async_cmd + async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if rgb and COLOR_MODE_RGB in self.supported_color_modes: + if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb: + _LOGGER.debug("RGB already set to: %s", rgb) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting RGB: %s", rgb) - self._bulb.set_rgb( - rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type + await self._bulb.async_set_rgb( + *rgb, duration=duration, light_type=self.light_type ) - @_cmd - def set_colortemp(self, colortemp, duration) -> None: + @_async_cmd + async def async_set_colortemp(self, colortemp, duration) -> None: """Set bulb's color temperature.""" if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) - _LOGGER.debug("Setting color temp: %s K", temp_in_k) - self._bulb.set_color_temp( + if ( + self.color_mode == COLOR_MODE_COLOR_TEMP + and self.color_temp == colortemp + ): + _LOGGER.debug("Color temp already set to: %s", temp_in_k) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + + await self._bulb.async_set_color_temp( temp_in_k, duration=duration, light_type=self.light_type ) - @_cmd - def set_default(self) -> None: + @_async_cmd + async def async_set_default(self) -> None: """Set current options as default.""" - self._bulb.set_default() + await self._bulb.async_set_default() - @_cmd - def set_flash(self, flash) -> None: + @_async_cmd + async def async_set_flash(self, flash) -> None: """Activate flash.""" if flash: if int(self._bulb.last_properties["color_mode"]) != 1: @@ -660,7 +662,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): count = 1 duration = transition * 2 - red, green, blue = color_util.color_hs_to_RGB(*self._hs) + red, green, blue = color_util.color_hs_to_RGB(*self.hs_color) transitions = [] transitions.append( @@ -675,18 +677,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): flow = Flow(count=count, transitions=transitions) try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set flash: %s", ex) - @_cmd - def set_effect(self, effect) -> None: + @_async_cmd + async def async_set_effect(self, effect) -> None: """Activate effect.""" if not effect: return if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) + await self._bulb.async_stop_flow(light_type=self.light_type) return if effect in self.custom_effects_names: @@ -705,12 +707,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) self._effect = effect except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -723,15 +725,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_on( - duration=duration, - light_type=self.light_type, - power_mode=self._turn_on_power_mode, - ) + if not self.is_on: + await self.device.async_turn_on( + duration=duration, + light_type=self.light_type, + power_mode=self._turn_on_power_mode, + ) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: - self.set_music_mode(self.config[CONF_MODE_MUSIC]) + await self.hass.async_add_executor_job( + self.set_music_mode, self.config[CONF_MODE_MUSIC] + ) except BulbException as ex: _LOGGER.error( "Unable to turn on music mode, consider disabling it: %s", ex @@ -739,12 +744,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): try: # values checked for none in methods - self.set_hs(hs_color, duration) - self.set_rgb(rgb, duration) - self.set_colortemp(colortemp, duration) - self.set_brightness(brightness, duration) - self.set_flash(flash) - self.set_effect(effect) + await self.async_set_hs(hs_color, duration) + await self.async_set_rgb(rgb, duration) + await self.async_set_colortemp(colortemp, duration) + await self.async_set_brightness(brightness, duration) + await self.async_set_flash(flash) + await self.async_set_effect(effect) except BulbException as ex: _LOGGER.error("Unable to set bulb properties: %s", ex) return @@ -752,50 +757,48 @@ class YeelightGenericLight(YeelightEntity, LightEntity): # save the current state if we had a manual change. if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): try: - self.set_default() + await self.async_set_default() except BulbException as ex: _LOGGER.error("Unable to set the defaults: %s", ex) return - self.device.update() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off.""" + if not self.is_on: + return + duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_off(duration=duration, light_type=self.light_type) - self.device.update() + await self.device.async_turn_off(duration=duration, light_type=self.light_type) - def set_mode(self, mode: str): + async def async_set_mode(self, mode: str): """Set a power mode.""" try: - self._bulb.set_power_mode(PowerMode[mode.upper()]) - self.device.update() + await self._bulb.async_set_power_mode(PowerMode[mode.upper()]) except BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) - def start_flow(self, transitions, count=0, action=ACTION_RECOVER): + async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" try: flow = Flow( count=count, action=Flow.actions[action], transitions=transitions ) - self._bulb.start_flow(flow, light_type=self.light_type) - self.device.update() + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def set_scene(self, scene_class, *args): + async def async_set_scene(self, scene_class, *args): """ Set the light directly to the specified state. If the light is off, it will first be turned on. """ try: - self._bulb.set_scene(scene_class, *args) - self.device.update() + await self._bulb.async_set_scene(scene_class, *args) except BulbException as ex: _LOGGER.error("Unable to set scene: %s", ex) @@ -902,7 +905,7 @@ class YeelightNightLightMode(YeelightGenericLight): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{self.device.name} nightlight" + return f"{self.device.name} Nightlight" @property def icon(self): @@ -994,7 +997,7 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{self.device.name} ambilight" + return f"{self.device.name} Ambilight" @property def _brightness_property(self): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 0bf6249b647..b1c1c131907 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,14 +2,15 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.3"], - "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], + "requirements": ["yeelight==0.7.2", "async-upnp-client==0.20.0"], + "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, - "iot_class": "local_polling", + "quality_scale": "platinum", + "iot_class": "local_push", "dhcp": [{ "hostname": "yeelink-*" }], "homekit": { - "models": ["YLDP*"] + "models": ["YLD*"] } } diff --git a/homeassistant/components/yeelight/translations/zh-Hans.json b/homeassistant/components/yeelight/translations/zh-Hans.json new file mode 100644 index 00000000000..43fb1d9fe25 --- /dev/null +++ b/homeassistant/components/yeelight/translations/zh-Hans.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "no_devices_found": "\u60a8\u7684\u7f51\u7edc\u672a\u53d1\u73b0 Yeelight \u8bbe\u5907" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "flow_title": "{model} {host}", + "step": { + "discovery_confirm": { + "description": "\u60a8\u8981\u8bbe\u7f6e {model} ( {host} )\u5417\uff1f" + }, + "pick_device": { + "data": { + "device": "\u8bbe\u5907" + } + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740" + }, + "description": "\u5982\u679c\u60a8\u5c06\u4e3b\u673a\u5730\u5740\u680f\u7559\u7a7a\uff0c\u5219\u5c06\u81ea\u52a8\u5bfb\u627e\u8bbe\u5907\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u578b\u53f7\uff08\u53ef\u9009\uff09", + "nightlight_switch": "\u4f7f\u7528\u591c\u5149\u5f00\u5173", + "save_on_change": "\u4fdd\u5b58\u66f4\u6539\u72b6\u6001", + "transition": "\u8fc7\u6e21\u65f6\u95f4\uff08\u6beb\u79d2\uff09", + "use_music_mode": "\u542f\u7528\u97f3\u4e50\u6a21\u5f0f" + }, + "description": "\u5982\u679c\u5c06\u4fe1\u53f7\u680f\u7559\u7a7a\uff0c\u96c6\u6210\u5c06\u4f1a\u81ea\u52a8\u68c0\u6d4b\u76f8\u5173\u4fe1\u606f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index c130532a2e1..91dfaab38bf 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -1,12 +1,13 @@ """Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" -import asyncio +from __future__ import annotations + import logging from aioftp import Client, StatusCodeError from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol +from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( @@ -119,15 +120,18 @@ class YiCamera(Camera): self._is_on = False return None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" url = await self._get_latest_video_url() if url and url != self._last_url: - ffmpeg = ImageFrame(self._manager.binary) - self._last_image = await asyncio.shield( - ffmpeg.get_image( - url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments - ), + self._last_image = await ffmpeg.async_get_image( + self.hass, + url, + extra_cmd=self._extra_arguments, + width=width, + height=height, ) self._last_url = url diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index d00f0457b85..1ea7bc67ba9 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -3,7 +3,7 @@ "name": "YouLess", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/youless", - "requirements": ["youless-api==0.10"], + "requirements": ["youless-api==0.12"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 54155034919..bc0f1ee873b 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -3,11 +3,11 @@ from __future__ import annotations from youless_api.youless_sensor import YoulessSensor +from homeassistant.components.sensor import SensorEntity from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, DEVICE_CLASS_POWER from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -40,7 +40,7 @@ async def async_setup_entry( ) -class YoulessBaseSensor(CoordinatorEntity, Entity): +class YoulessBaseSensor(CoordinatorEntity, SensorEntity): """The base sensor for Youless.""" def __init__( @@ -71,7 +71,7 @@ class YoulessBaseSensor(CoordinatorEntity, Entity): return None @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement for the sensor.""" if self.get_sensor is None: return None @@ -79,7 +79,7 @@ class YoulessBaseSensor(CoordinatorEntity, Entity): return self.get_sensor.unit_of_measurement @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Determine the state value, only if a sensor is initialized.""" if self.get_sensor is None: return None diff --git a/homeassistant/components/youless/translations/es.json b/homeassistant/components/youless/translations/es.json new file mode 100644 index 00000000000..72a56cc5608 --- /dev/null +++ b/homeassistant/components/youless/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Anfitri\u00f3n", + "name": "Nombre" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/hu.json b/homeassistant/components/youless/translations/hu.json new file mode 100644 index 00000000000..21c7a7ebe4b --- /dev/null +++ b/homeassistant/components/youless/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni" + }, + "step": { + "user": { + "data": { + "host": "H\u00e1zigazda", + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/no.json b/homeassistant/components/youless/translations/no.json index 01ea5b65fb1..460c07cb535 100644 --- a/homeassistant/components/youless/translations/no.json +++ b/homeassistant/components/youless/translations/no.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, "step": { "user": { "data": { + "host": "Vert", "name": "Navn" } } diff --git a/homeassistant/components/youless/translations/zh-Hans.json b/homeassistant/components/youless/translations/zh-Hans.json new file mode 100644 index 00000000000..cfe90f18df3 --- /dev/null +++ b/homeassistant/components/youless/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index a2644287690..ff2e2c4d9ba 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -94,12 +94,12 @@ class ZabbixTriggerCountSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return "issues" diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index a6018de831e..5659e4835db 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -171,12 +171,12 @@ class ZamgSensor(SensorEntity): return f"{self.client_name} {self.variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.probe.get_data(self.variable) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self.variable][1] diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 4c4c81aff32..8b1f482e05e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Coroutine from contextlib import suppress import fnmatch -import ipaddress +from ipaddress import IPv6Address, ip_address import logging import socket from typing import Any, TypedDict, cast @@ -28,6 +28,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf @@ -130,34 +131,11 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero return aio_zc -def _async_use_default_interface(adapters: list[Adapter]) -> bool: - for adapter in adapters: - if adapter["enabled"] and not adapter["default"]: - return False - return True - - -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" zc_args: dict = {} adapters = await network.async_get_adapters(hass) - if _async_use_default_interface(adapters): - zc_args["interfaces"] = InterfaceChoice.Default - else: - interfaces = zc_args["interfaces"] = [] - for adapter in adapters: - if not adapter["enabled"]: - continue - if ipv4s := adapter["ipv4"]: - interfaces.extend( - ipv4["address"] - 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) ipv6 = True if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): @@ -166,6 +144,16 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: else: zc_args["ip_version"] = IPVersion.All + if not ipv6 and network.async_only_default_interface_enabled(adapters): + zc_args["interfaces"] = InterfaceChoice.Default + else: + zc_args["interfaces"] = [ + str(source_ip) + for source_ip in await network.async_get_enabled_source_ips(hass) + if not source_ip.is_loopback + and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + ] + aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf_types, homekit_models = await asyncio.gather( @@ -208,7 +196,7 @@ def _get_announced_addresses( addresses = { addr.packed for addr in [ - ipaddress.ip_address(ip["address"]) + ip_address(ip["address"]) for adapter in adapters if adapter["enabled"] for ip in cast(list, adapter["ipv6"]) + cast(list, adapter["ipv4"]) @@ -525,7 +513,7 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: address = service.addresses[0] return { - "host": str(ipaddress.ip_address(address)), + "host": str(ip_address(address)), "port": service.port, "hostname": service.server, "type": service.type, diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ee1e9a8e1ab..84f9f4698e9 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.36.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0333bb76a20..bac32563776 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -77,7 +77,7 @@ class ZestimateDataSensor(SensorEntity): return f"{self._name} {self.address}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" try: return round(float(self._state), 1) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 628d9c3b9be..a340ffae736 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -523,7 +523,7 @@ class Light(BaseLight, ZhaEntity): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers="Philips", + manufacturers={"Philips", "Signify Netherlands B.V."}, ) class HueLight(Light): """Representation of a HUE light which does not report attributes.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3c3aba919ed..cc401cb1e05 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -135,12 +135,12 @@ class Sensor(ZhaEntity, SensorEntity): return self._state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._unit @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" assert self.SENSOR_ATTR is not None raw_state = self._channel.cluster.get(self.SENSOR_ATTR) @@ -274,7 +274,7 @@ class SmartEnergyMetering(Sensor): return self._channel.formatter_function(value) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return Unit of measurement.""" return self._channel.unit_of_measurement diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 2b078092ed7..9722095b548 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -8,13 +8,27 @@ }, "flow_title": "{name}", "step": { + "pick_radio": { + "data": { + "radio_type": "R\u00e1di\u00f3 t\u00edpusa" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 t\u00edpus\u00e1t", + "title": "R\u00e1di\u00f3 t\u00edpusa" + }, "port_config": { "data": { - "baudrate": "port sebess\u00e9g" + "baudrate": "port sebess\u00e9g", + "flow_control": "adat\u00e1raml\u00e1s szab\u00e1lyoz\u00e1sa", + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" }, + "description": "Adja meg a port specifikus be\u00e1ll\u00edt\u00e1sokat", "title": "Be\u00e1ll\u00edt\u00e1sok" }, "user": { + "data": { + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 soros portj\u00e1t", "title": "ZHA" } } @@ -35,11 +49,59 @@ } }, "device_automation": { + "action_type": { + "squawk": "Riaszt\u00e1s", + "warn": "Figyelmeztet\u00e9s" + }, "trigger_subtype": { - "turn_off": "Kikapcsol\u00e1s" + "both_buttons": "Mindk\u00e9t gomb", + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "button_5": "\u00d6t\u00f6dik gomb", + "button_6": "Hatodik gomb", + "close": "Bez\u00e1r\u00e1s", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "face_1": "aktiv\u00e1lt 1 arccal", + "face_2": "aktiv\u00e1lt 2 arccal", + "face_3": "aktiv\u00e1lt 3 arccal", + "face_4": "aktiv\u00e1lt 4 arccal", + "face_5": "aktiv\u00e1lt 5 arccal", + "face_6": "aktiv\u00e1lt 6 arccal", + "face_any": "B\u00e1rmely/meghat\u00e1rozott arc(ok) aktiv\u00e1l\u00e1s\u00e1val", + "left": "Bal", + "open": "Nyitva", + "right": "Jobb", + "turn_off": "Kikapcsol\u00e1s", + "turn_on": "Bekapcsol\u00e1s" }, "trigger_type": { - "device_offline": "Eszk\u00f6z offline" + "device_dropped": "A k\u00e9sz\u00fcl\u00e9k eldobva", + "device_flipped": "Eszk\u00f6z \u00e1tford\u00edtva \"{subtype}\"", + "device_knocked": "Az eszk\u00f6zt le\u00fct\u00f6tt\u00e9k \"{subtype}\"", + "device_offline": "Eszk\u00f6z offline", + "device_rotated": "Eszk\u00f6z elforgatva \"{subtype}\"", + "device_shaken": "A k\u00e9sz\u00fcl\u00e9k megr\u00e1zk\u00f3dott", + "device_slid": "Eszk\u00f6z cs\u00fasztatott \"{subtype}\"", + "device_tilted": "K\u00e9sz\u00fcl\u00e9k megd\u00f6ntve", + "remote_button_alt_double_press": "A \u201e{subtype}\u201d gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", + "remote_button_alt_long_press": "\"{subtype}\" gomb folyamatosan nyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_long_release": "A \u201e{subtype}\u201d gomb elenged\u00e9se hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", + "remote_button_alt_quadruple_press": "A \u201e{subtype}\u201d gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_press": "\u201e{subtype}\u201d gomb lenyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_release": "A \"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", + "remote_button_alt_triple_press": "A \u201e{subtype}\u201d gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak", + "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva", + "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", + "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", + "remote_button_short_press": "\"{subtype}\" gomb lenyomva", + "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nn.json b/homeassistant/components/zha/translations/nn.json index 2e607435b7e..9e9b677ddc1 100644 --- a/homeassistant/components/zha/translations/nn.json +++ b/homeassistant/components/zha/translations/nn.json @@ -1,6 +1,9 @@ { "config": { "step": { + "port_config": { + "title": "Innstillinger" + }, "user": { "title": "ZHA" } diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index 80a4f782915..b337dda1db0 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -196,7 +196,7 @@ class ZodiacSensor(SensorEntity): return "zodiac__sign" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the device.""" return self._state diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 8ab0e9b2703..d4474d793ab 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers import ( service, storage, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -176,7 +177,7 @@ class ZoneStorageCollection(collection.StorageCollection): return {**data, **update_data} -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up configured zones as well as Home Assistant zone if necessary.""" component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 701f4b490d3..d392901b633 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -71,7 +71,7 @@ class ZMSensorMonitors(SensorEntity): return f"{self._monitor.name} Status" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -107,12 +107,12 @@ class ZMSensorEvents(SensorEntity): return f"{self._monitor.name} {self.time_period.title}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "Events" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -136,7 +136,7 @@ class ZMSensorRunState(SensorEntity): return "Run State" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/zoneminder/translations/zh-Hans.json b/homeassistant/components/zoneminder/translations/zh-Hans.json index a5f4ff11f09..8f3265d4344 100644 --- a/homeassistant/components/zoneminder/translations/zh-Hans.json +++ b/homeassistant/components/zoneminder/translations/zh-Hans.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u81f3 ZoneMinder \u670d\u52a1\u5668\u3002" + }, + "error": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u81f3 ZoneMinder \u670d\u52a1\u5668\u3002" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index d973e52ff92..75046c2f9d8 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -56,12 +56,12 @@ class ZWaveSensor(ZWaveDeviceEntity, SensorEntity): return True @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement the value is expressed in.""" return self._units @@ -70,7 +70,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): """Representation of a multi level sensor Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._units in ("C", "F"): return round(self._state, 1) @@ -87,7 +87,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._units == "C": return TEMP_CELSIUS diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6320efddb60..c8f2bd19776 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -28,6 +28,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api @@ -79,7 +80,7 @@ DATA_CONNECT_FAILED_LOGGED = "connect_failed_logged" DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged" -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Z-Wave JS component.""" hass.data[DOMAIN] = {} return True diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index b419230a0bd..4ae8142ec9e 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -197,7 +197,10 @@ async def async_get_condition_capabilities( "extra_fields": vol.Schema( { vol.Required(ATTR_COMMAND_CLASS): vol.In( - {cc.value: cc.name for cc in CommandClass} + { + CommandClass(cc.id).value: CommandClass(cc.id).name + for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + } ), vol.Required(ATTR_PROPERTY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string, diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 588b4c76472..d59a3d935a0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -359,24 +359,11 @@ DISCOVERY_SCHEMAS = [ get_config_parameter_discovery_schema( property_name={"Door lock mode"}, device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks ZWaveDiscoverySchema( platform="lock", - device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.LOCK, @@ -390,13 +377,6 @@ DISCOVERY_SCHEMAS = [ ZWaveDiscoverySchema( platform="binary_sensor", hint="property", - device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.LOCK, @@ -542,10 +522,10 @@ DISCOVERY_SCHEMAS = [ allow_multi=True, entity_registry_enabled_default=False, ), - # sensor for basic CC + # number for Basic CC ZWaveDiscoverySchema( - platform="sensor", - hint="numeric_sensor", + platform="number", + hint="Basic", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.BASIC, @@ -553,6 +533,15 @@ DISCOVERY_SCHEMAS = [ type={"number"}, property={"currentValue"}, ), + required_values=[ + ZWaveValueDiscoverySchema( + command_class={ + CommandClass.BASIC, + }, + type={"number"}, + property={"targetValue"}, + ) + ], entity_registry_enabled_default=False, ), # binary switches @@ -633,6 +622,40 @@ DISCOVERY_SCHEMAS = [ platform="siren", primary_value=SIREN_TONE_SCHEMA, ), + # select + # siren default tone + ZWaveDiscoverySchema( + platform="select", + hint="Default tone", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SOUND_SWITCH}, + property={"defaultToneId"}, + type={"number"}, + ), + required_values=[SIREN_TONE_SCHEMA], + ), + # number + # siren default volume + ZWaveDiscoverySchema( + platform="number", + hint="volume", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SOUND_SWITCH}, + property={"defaultVolume"}, + type={"number"}, + ), + required_values=[SIREN_TONE_SCHEMA], + ), + # select + # protection CC + ZWaveDiscoverySchema( + platform="select", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.PROTECTION}, + property={"local", "rf"}, + type={"number"}, + ), + ), ] diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index f3cabe8b6a7..91a7f191e5d 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -287,39 +287,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): else: zwave_transition = {TRANSITION_DURATION: "default"} - if combined_color_val and isinstance(combined_color_val.value, dict): - colors_dict = {} - for color, value in colors.items(): - color_name = MULTI_COLOR_MAP[color] - colors_dict[color_name] = value - # set updated color object - await self.info.node.async_set_value( - combined_color_val, colors_dict, zwave_transition - ) - return - - # fallback to setting the color(s) one by one if multicolor fails - # not sure this is needed at all, but just in case + colors_dict = {} for color, value in colors.items(): - await self._async_set_color(color, value, zwave_transition) - - async def _async_set_color( - self, - color: ColorComponent, - new_value: int, - transition: dict[str, str] | None = None, - ) -> None: - """Set defined color to given value.""" - # actually set the new color value - target_zwave_value = self.get_zwave_value( - "targetColor", - CommandClass.SWITCH_COLOR, - value_property_key=color.value, + color_name = MULTI_COLOR_MAP[color] + colors_dict[color_name] = value + # set updated color object + await self.info.node.async_set_value( + combined_color_val, colors_dict, zwave_transition ) - if target_zwave_value is None: - # guard for unsupported color - return - await self.info.node.async_set_value(target_zwave_value, new_value, transition) async def _async_set_brightness( self, brightness: int | None, transition: float | None = None diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index e53e5942999..675a396fb7b 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -26,7 +26,10 @@ async def async_setup_entry( def async_add_number(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave number entity.""" entities: list[ZWaveBaseEntity] = [] - entities.append(ZwaveNumberEntity(config_entry, client, info)) + if info.platform_hint == "volume": + entities.append(ZwaveVolumeNumberEntity(config_entry, client, info)) + else: + entities.append(ZwaveNumberEntity(config_entry, client, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -87,3 +90,38 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" await self.info.node.async_set_value(self._target_value, value) + + +class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): + """Representation of a volume number entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveVolumeNumberEntity entity.""" + super().__init__(config_entry, client, info) + self.correction_factor = int( + self.info.primary_value.metadata.max - self.info.primary_value.metadata.min + ) + # Fallback in case we can't properly calculate correction factor + if self.correction_factor == 0: + self.correction_factor = 1 + + # Entity class attributes + self._attr_min_value = 0 + self._attr_max_value = 1 + self._attr_step = 0.01 + self._attr_name = self.generate_name(include_value_name=True) + + @property + def value(self) -> float | None: + """Return the entity value.""" + if self.info.primary_value.value is None: + return None + return float(self.info.primary_value.value) / self.correction_factor + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self.info.node.async_set_value( + self.info.primary_value, round(value * self.correction_factor) + ) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py new file mode 100644 index 00000000000..7aedc6521d9 --- /dev/null +++ b/homeassistant/components/zwave_js/select.py @@ -0,0 +1,127 @@ +"""Support for Z-Wave controls using the select platform.""" +from __future__ import annotations + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass, ToneID + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_CLIENT, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave Select entity from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_select(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave select entity.""" + entities: list[ZWaveBaseEntity] = [] + if info.platform_hint == "Default tone": + entities.append(ZwaveDefaultToneSelectEntity(config_entry, client, info)) + else: + entities.append(ZwaveSelectEntity(config_entry, client, info)) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{SELECT_DOMAIN}", + async_add_select, + ) + ) + + +class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): + """Representation of a Z-Wave select entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveSelectEntity entity.""" + super().__init__(config_entry, client, info) + + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + self._attr_options = list(self.info.primary_value.metadata.states.values()) + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self.info.primary_value.value is None: + return None + return str( + self.info.primary_value.metadata.states.get( + str(self.info.primary_value.value), self.info.primary_value.value + ) + ) + + async def async_select_option(self, option: str | int) -> None: + """Change the selected option.""" + key = next( + key + for key, val in self.info.primary_value.metadata.states.items() + if val == option + ) + await self.info.node.async_set_value(self.info.primary_value, int(key)) + + +class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): + """Representation of a Z-Wave default tone select entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveDefaultToneSelectEntity entity.""" + super().__init__(config_entry, client, info) + self._tones_value = self.get_zwave_value( + "toneId", command_class=CommandClass.SOUND_SWITCH + ) + + # Entity class attributes + self._attr_name = self.generate_name( + include_value_name=True, alternate_value_name=info.platform_hint + ) + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + return [ + val + for key, val in self._tones_value.metadata.states.items() + if int(key) not in (ToneID.DEFAULT, ToneID.OFF) + ] + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + return str( + self._tones_value.metadata.states.get( + str(self.info.primary_value.value), self.info.primary_value.value + ) + ) + + async def async_select_option(self, option: str | int) -> None: + """Change the selected option.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + key = next( + key + for key, val in self._tones_value.metadata.states.items() + if val == option + ) + await self.info.node.async_set_value(self.info.primary_value, int(key)) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 7b491661e68..1ffa263dae7 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -11,13 +11,13 @@ 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, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -31,13 +31,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ -181,14 +176,14 @@ class ZWaveStringSensor(ZwaveSensorBase): """Representation of a Z-Wave String sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None return str(self.info.primary_value.value) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -198,31 +193,15 @@ class ZWaveStringSensor(ZwaveSensorBase): class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor.""" - def __init__( - self, - config_entry: ConfigEntry, - client: ZwaveClient, - info: ZwaveDiscoveryInfo, - ) -> None: - """Initialize a ZWaveNumericSensor entity.""" - super().__init__(config_entry, client, info) - - # Entity class attributes - if self.info.primary_value.command_class == CommandClass.BASIC: - self._attr_name = self.generate_name( - include_value_name=True, - alternate_value_name=self.info.primary_value.command_class_name, - ) - @property - def state(self) -> float: + def native_value(self) -> float: """Return state of the sensor.""" if self.info.primary_value.value is None: return 0 return round(float(self.info.primary_value.value), 2) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -234,7 +213,7 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) -class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): +class ZWaveMeterSensor(ZWaveNumericSensor): """Representation of a Z-Wave Meter CC sensor.""" def __init__( @@ -247,51 +226,10 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): super().__init__(config_entry, client, info) # Entity class attributes - self._attr_state_class = STATE_CLASS_MEASUREMENT 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, - # or a meter type was specified and doesn't match this entity's meter type: - if ( - 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 - ): - 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() - - # 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: - 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, - ) - ) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + else: + self._attr_state_class = STATE_CLASS_MEASUREMENT async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None @@ -315,15 +253,6 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): options, ) - # 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): """Representation of a Z-Wave Numeric sensor with multiple states.""" @@ -345,7 +274,7 @@ class ZWaveListSensor(ZwaveSensorBase): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None @@ -387,7 +316,7 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None @@ -439,7 +368,7 @@ class ZWaveNodeStatusSensor(SensorEntity): self._attr_device_info = { "identifiers": {get_device_id(self.client, self.node)}, } - self._attr_state: str = node.status.name.lower() + self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -447,7 +376,7 @@ class ZWaveNodeStatusSensor(SensorEntity): def _status_changed(self, _: dict) -> None: """Call when status event is received.""" - self._attr_state = self.node.status.name.lower() + self._attr_native_value = self.node.status.name.lower() self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index de74f55fa9a..c1b354f4faa 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -58,9 +58,9 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): """Initialize a ZwaveSirenEntity entity.""" super().__init__(config_entry, client, info) # Entity class attributes - self._attr_available_tones = list( - self.info.primary_value.metadata.states.values() - ) + self._attr_available_tones = { + int(id): val for id, val in self.info.primary_value.metadata.states.items() + } self._attr_supported_features = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET ) @@ -82,23 +82,15 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - tone: str | None = kwargs.get(ATTR_TONE) + tone_id: int | None = kwargs.get(ATTR_TONE) options = {} if (volume := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: options["volume"] = round(volume * 100) # Play the default tone if a tone isn't provided - if tone is None: + if tone_id is None: await self.async_set_value(ToneID.DEFAULT, options) return - tone_id = int( - next( - key - for key, value in self.info.primary_value.metadata.states.items() - if value == tone - ) - ) - await self.async_set_value(tone_id, options) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 9f8af44c451..05efdb8e5ff 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -21,5 +21,20 @@ } } } + }, + "device_automation": { + "condition_type": { + "config_parameter": "Hodnota konfigura\u010dn\u00edho parametru {subtype}", + "node_status": "Stav uzlu", + "value": "Aktu\u00e1ln\u00ed hodnota Z-Wave hodnoty" + }, + "trigger_type": { + "event.notification.entry_control": "Odeslat ozn\u00e1men\u00ed o \u0159\u00edzen\u00ed vstupu", + "event.notification.notification": "Odeslal ozn\u00e1men\u00ed", + "event.value_notification.basic": "Z\u00e1kladn\u00ed ud\u00e1lost CC na {subtype}", + "event.value_notification.central_scene": "Akce centr\u00e1ln\u00ed sc\u00e9ny na {subtype}", + "event.value_notification.scene_activation": "Aktivace sc\u00e9ny na {subtype}", + "state.node_status": "Stav uzlu zm\u011bn\u011bn" + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 8eb4c176356..34ddeb753b1 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -51,6 +51,21 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Konfigurer parameter {subtype} verdi", + "node_status": "Nodestatus", + "value": "Gjeldende verdi for en Z-Wave-verdi" + }, + "trigger_type": { + "event.notification.entry_control": "Sendte et varsel om oppf\u00f8ringskontroll", + "event.notification.notification": "Sendte et varsel", + "event.value_notification.basic": "Grunnleggende CC -hendelse p\u00e5 {subtype}", + "event.value_notification.central_scene": "Sentral scenehandling p\u00e5 {subtype}", + "event.value_notification.scene_activation": "Sceneaktivering p\u00e5 {subtype}", + "state.node_status": "Nodestatus endret" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", diff --git a/homeassistant/config.py b/homeassistant/config.py index 12a39ab291b..754420dbcce 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -10,6 +10,7 @@ import re import shutil from types import ModuleType from typing import Any, Callable +from urllib.parse import urlparse from awesomeversion import AwesomeVersion import voluptuous as vol @@ -161,6 +162,19 @@ def _no_duplicate_auth_mfa_module( return configs +def _filter_bad_internal_external_urls(conf: dict) -> dict: + """Filter internal/external URL with a path.""" + for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL: + if key in conf and urlparse(conf[key]).path not in ("", "/"): + # We warn but do not fix, because if this was incorrectly configured, + # adjusting this value might impact security. + _LOGGER.warning( + "Invalid %s set. It's not allowed to have a path (/bla)", key + ) + + return conf + + PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config ) @@ -188,59 +202,64 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( } ) -CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( - { - CONF_NAME: vol.Coerce(str), - CONF_LATITUDE: cv.latitude, - CONF_LONGITUDE: cv.longitude, - CONF_ELEVATION: vol.Coerce(int), - vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, - CONF_UNIT_SYSTEM: cv.unit_system, - CONF_TIME_ZONE: cv.time_zone, - vol.Optional(CONF_INTERNAL_URL): cv.url, - vol.Optional(CONF_EXTERNAL_URL): cv.url, - vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter - ), - vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter - ), - vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(cv.ensure_list, [cv.url]), - vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, - vol.Optional(CONF_AUTH_PROVIDERS): vol.All( - cv.ensure_list, - [ - auth_providers.AUTH_PROVIDER_SCHEMA.extend( - { - CONF_TYPE: vol.NotIn( - ["insecure_example"], - "The insecure_example auth provider" - " is for testing only.", - ) - } - ) - ], - _no_duplicate_auth_provider, - ), - vol.Optional(CONF_AUTH_MFA_MODULES): vol.All( - cv.ensure_list, - [ - auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( - { - CONF_TYPE: vol.NotIn( - ["insecure_example"], - "The insecure_example mfa module is for testing only.", - ) - } - ) - ], - _no_duplicate_auth_mfa_module, - ), - # pylint: disable=no-value-for-parameter - vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), - vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, - vol.Optional(CONF_CURRENCY): cv.currency, - } +CORE_CONFIG_SCHEMA = vol.All( + CUSTOMIZE_CONFIG_SCHEMA.extend( + { + CONF_NAME: vol.Coerce(str), + CONF_LATITUDE: cv.latitude, + CONF_LONGITUDE: cv.longitude, + CONF_ELEVATION: vol.Coerce(int), + vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, + CONF_UNIT_SYSTEM: cv.unit_system, + CONF_TIME_ZONE: cv.time_zone, + vol.Optional(CONF_INTERNAL_URL): cv.url, + vol.Optional(CONF_EXTERNAL_URL): cv.url, + vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All( + cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + ), + vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( + cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + ), + vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All( + cv.ensure_list, [cv.url] + ), + vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, + vol.Optional(CONF_AUTH_PROVIDERS): vol.All( + cv.ensure_list, + [ + auth_providers.AUTH_PROVIDER_SCHEMA.extend( + { + CONF_TYPE: vol.NotIn( + ["insecure_example"], + "The insecure_example auth provider" + " is for testing only.", + ) + } + ) + ], + _no_duplicate_auth_provider, + ), + vol.Optional(CONF_AUTH_MFA_MODULES): vol.All( + cv.ensure_list, + [ + auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + CONF_TYPE: vol.NotIn( + ["insecure_example"], + "The insecure_example mfa module is for testing only.", + ) + } + ) + ], + _no_duplicate_auth_mfa_module, + ), + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), + vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, + vol.Optional(CONF_CURRENCY): cv.currency, + } + ), + _filter_bad_internal_external_urls, ) @@ -906,7 +925,7 @@ async def async_process_component_config( # noqa: C901 @callback -def config_without_domain(config: dict, domain: str) -> dict: +def config_without_domain(config: ConfigType, domain: str) -> ConfigType: """Return a config with all configuration for a domain removed.""" filter_keys = extract_domain_configs(config, domain) return {key: value for key, value in config.items() if key not in filter_keys} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ec074f81b95..07cb9eae7f9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -21,7 +21,12 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event -from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType +from homeassistant.helpers.typing import ( + UNDEFINED, + ConfigType, + DiscoveryInfoType, + UndefinedType, +) from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util @@ -598,7 +603,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """Manage all the config entry flows that are in progress.""" def __init__( - self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict + self, + hass: HomeAssistant, + config_entries: ConfigEntries, + hass_config: ConfigType, ) -> None: """Initialize the config entry flow manager.""" super().__init__(hass) @@ -748,7 +756,7 @@ class ConfigEntries: An instance of this object is available via `hass.config_entries`. """ - def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: + def __init__(self, hass: HomeAssistant, hass_config: ConfigType) -> None: """Initialize the entry manager.""" self.hass = hass self.flow = ConfigEntriesFlowManager(hass, self, hass_config) diff --git a/homeassistant/const.py b/homeassistant/const.py index ccd42ca32bb..ae1f50d0087 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -232,6 +232,7 @@ EVENT_TIME_CHANGED: Final = "time_changed" # #### DEVICE CLASSES #### +DEVICE_CLASS_AQI: Final = "aqi" DEVICE_CLASS_BATTERY: Final = "battery" DEVICE_CLASS_CO: Final = "carbon_monoxide" DEVICE_CLASS_CO2: Final = "carbon_dioxide" @@ -240,13 +241,22 @@ DEVICE_CLASS_ENERGY: Final = "energy" DEVICE_CLASS_HUMIDITY: Final = "humidity" DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" DEVICE_CLASS_MONETARY: Final = "monetary" +DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" +DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" +DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OZONE: Final = "ozone" DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" DEVICE_CLASS_POWER: Final = "power" +DEVICE_CLASS_PM25: Final = "pm25" +DEVICE_CLASS_PM1: Final = "pm1" +DEVICE_CLASS_PM10: Final = "pm10" DEVICE_CLASS_PRESSURE: Final = "pressure" DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" +DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" DEVICE_CLASS_TEMPERATURE: Final = "temperature" DEVICE_CLASS_TIMESTAMP: Final = "timestamp" DEVICE_CLASS_VOLTAGE: Final = "voltage" +DEVICE_CLASS_GAS: Final = "gas" # #### STATES #### STATE_ON: Final = "on" @@ -612,6 +622,7 @@ SERVICE_CLOSE_COVER: Final = "close_cover" SERVICE_CLOSE_COVER_TILT: Final = "close_cover_tilt" SERVICE_OPEN_COVER: Final = "open_cover" SERVICE_OPEN_COVER_TILT: Final = "open_cover_tilt" +SERVICE_SAVE_PERSISTENT_STATES: Final = "save_persistent_states" SERVICE_SET_COVER_POSITION: Final = "set_cover_position" SERVICE_SET_COVER_TILT_POSITION: Final = "set_cover_tilt_position" SERVICE_STOP_COVER: Final = "stop_cover" diff --git a/homeassistant/core.py b/homeassistant/core.py index e2418321592..1b1849ba548 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -19,6 +19,7 @@ import threading from time import monotonic from types import MappingProxyType from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast +from urllib.parse import urlparse import attr import voluptuous as vol @@ -1717,19 +1718,35 @@ class Config: ) data = await store.async_load() - if data: - self._update( - source=SOURCE_STORAGE, - latitude=data.get("latitude"), - longitude=data.get("longitude"), - elevation=data.get("elevation"), - unit_system=data.get("unit_system"), - location_name=data.get("location_name"), - time_zone=data.get("time_zone"), - external_url=data.get("external_url", _UNDEF), - internal_url=data.get("internal_url", _UNDEF), - currency=data.get("currency"), - ) + if not data: + return + + # In 2021.9 we fixed validation to disallow a path (because that's never correct) + # but this data still lives in storage, so we print a warning. + if data.get("external_url") and urlparse(data["external_url"]).path not in ( + "", + "/", + ): + _LOGGER.warning("Invalid external_url set. It's not allowed to have a path") + + if data.get("internal_url") and urlparse(data["internal_url"]).path not in ( + "", + "/", + ): + _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path") + + self._update( + source=SOURCE_STORAGE, + latitude=data.get("latitude"), + longitude=data.get("longitude"), + elevation=data.get("elevation"), + unit_system=data.get("unit_system"), + location_name=data.get("location_name"), + time_zone=data.get("time_zone"), + external_url=data.get("external_url", _UNDEF), + internal_url=data.get("internal_url", _UNDEF), + currency=data.get("currency"), + ) async def async_store(self) -> None: """Store [homeassistant] core config.""" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 844fd369cac..2a82c2652ed 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -9,6 +9,8 @@ import attr if TYPE_CHECKING: from .core import Context +# mypy: disallow-any-generics + class HomeAssistantError(Exception): """General Home Assistant exception occurred.""" @@ -42,7 +44,7 @@ class ConditionError(HomeAssistantError): """Return indentation.""" return " " * indent + message - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" raise NotImplementedError() @@ -58,7 +60,7 @@ class ConditionErrorMessage(ConditionError): # A message describing this error message: str = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" yield self._indent(indent, f"In '{self.type}' condition: {self.message}") @@ -74,7 +76,7 @@ class ConditionErrorIndex(ConditionError): # The error that this error wraps error: ConditionError = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" if self.total > 1: yield self._indent( @@ -93,7 +95,7 @@ class ConditionErrorContainer(ConditionError): # List of ConditionErrors that this error wraps errors: Sequence[ConditionError] = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" for item in self.errors: yield from item.output(indent) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d125f507d3a..6be4f70b38e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ FLOWS = [ "agent_dvr", "airly", "airnow", + "airtouch4", "airvisual", "alarmdecoder", "almond", @@ -180,6 +181,7 @@ FLOWS = [ "nexia", "nfandroidtv", "nightscout", + "nmap_tracker", "notion", "nuheat", "nuki", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index dbdaaf6da5e..d6b4fc4e457 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -16,6 +16,11 @@ DHCP = [ "hostname": "connect", "macaddress": "B8B7F1*" }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "2C9FFB*" + }, { "domain": "august", "hostname": "august*", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 536485f7f55..d973698a34b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -262,7 +262,7 @@ HOMEKIT = { "Touch HD": "rainmachine", "Welcome": "netatmo", "Wemo": "wemo", - "YLDP*": "yeelight", + "YLD*": "yeelight", "iSmartGate": "gogogate2", "iZone": "izone", "tado": "tado" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 66d1c01d6d3..f8d69a6e49a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -649,6 +649,16 @@ def url(value: Any) -> str: raise vol.Invalid("invalid url") +def url_no_path(value: Any) -> str: + """Validate a url without a path.""" + url_in = url(value) + + if urlparse(url_in).path not in ("", "/"): + raise vol.Invalid("url it not allowed to have a path component") + + return url_in + + def x10_address(value: str) -> str: """Validate an x10 address.""" regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6383de15b4a..63e84371fad 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -165,13 +165,13 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" - name: str + name: str | None connections: set[tuple[str, str]] identifiers: set[tuple[str, str]] - manufacturer: str - model: str - suggested_area: str - sw_version: str + manufacturer: str | None + model: str | None + suggested_area: str | None + sw_version: str | None via_device: tuple[str, str] entry_type: str | None default_name: str @@ -539,25 +539,13 @@ class Entity(ABC): if end - start > 0.4 and not self._slow_reported: self._slow_reported = True - extra = "" - if "custom_components" in type(self).__module__: - extra = "Please report it to the custom component author." - else: - extra = ( - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - if self.platform: - extra += ( - f"+label%3A%22integration%3A+{self.platform.platform_name}%22" - ) - + report_issue = self._suggest_report_issue() _LOGGER.warning( - "Updating state for %s (%s) took %.3f seconds. %s", + "Updating state for %s (%s) took %.3f seconds. Please %s", self.entity_id, type(self), end - start, - extra, + report_issue, ) # Overwrite properties that have been set in the config file. @@ -858,6 +846,23 @@ class Entity(ABC): if self.parallel_updates: self.parallel_updates.release() + def _suggest_report_issue(self) -> str: + """Suggest to report an issue.""" + report_issue = "" + if "custom_components" in type(self).__module__: + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + if self.platform: + report_issue += ( + f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + ) + + return report_issue + @dataclass class ToggleEntityDescription(EntityDescription): diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index da6c6935b35..cedd07676ba 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable import logging +from typing import Any from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -15,11 +16,13 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) async def async_reload_integration_platforms( - hass: HomeAssistant, integration_name: str, integration_platforms: Iterable + hass: HomeAssistant, integration_name: str, integration_platforms: Iterable[str] ) -> None: """Reload an integration's platforms. @@ -62,7 +65,7 @@ async def _resetup_platform( if not conf: return - root_config: dict = {integration_platform: []} + root_config: dict[str, Any] = {integration_platform: []} # Extract only the config for template, ignore the rest. for p_type, p_config in config_per_platform(conf, integration_platform): if p_type != integration_name: @@ -102,7 +105,7 @@ async def _async_setup_platform( hass: HomeAssistant, integration_name: str, integration_platform: str, - platform_configs: list[dict], + platform_configs: list[dict[str, Any]], ) -> None: """Platform for the first time when new configuration is added.""" if integration_platform not in hass.data: @@ -120,7 +123,7 @@ async def _async_setup_platform( async def _async_reconfig_platform( - platform: EntityPlatform, platform_configs: list[dict] + platform: EntityPlatform, platform_configs: list[dict[str, Any]] ) -> None: """Reconfigure an already loaded platform.""" await platform.async_reset() @@ -155,7 +158,7 @@ def async_get_platform_without_config_entry( async def async_setup_reload_service( - hass: HomeAssistant, domain: str, platforms: Iterable + hass: HomeAssistant, domain: str, platforms: Iterable[str] ) -> None: """Create the reload service for the domain.""" if hass.services.has_service(domain, SERVICE_RELOAD): @@ -171,7 +174,9 @@ async def async_setup_reload_service( ) -def setup_reload_service(hass: HomeAssistant, domain: str, platforms: Iterable) -> None: +def setup_reload_service( + hass: HomeAssistant, domain: str, platforms: Iterable[str] +) -> None: """Sync version of async_setup_reload_service.""" asyncio.run_coroutine_threadsafe( async_setup_reload_service(hass, domain, platforms), diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 67b2d329af1..da4d2bacf15 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -100,6 +100,12 @@ class RestoreStateData: return cast(RestoreStateData, await load_instance(hass)) + @classmethod + async def async_save_persistent_states(cls, hass: HomeAssistant) -> None: + """Dump states now.""" + data = await cls.async_get_instance(hass) + await data.async_dump_states() + def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 23241f22d1e..3dae84166f6 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -8,6 +8,8 @@ from homeassistant.core import HomeAssistant, callback from . import template +# mypy: disallow-any-generics + class ScriptVariables: """Class to hold and render script variables.""" @@ -65,6 +67,6 @@ class ScriptVariables: return rendered_variables - def as_dict(self) -> dict: + def as_dict(self) -> dict[str, Any]: """Return dict version of this class.""" return self.variables diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 66354aa7aa6..08b3956a490 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -22,7 +22,7 @@ from urllib.parse import urlencode as urllib_urlencode import weakref import jinja2 -from jinja2 import contextfunction, pass_context +from jinja2 import pass_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace import voluptuous as vol @@ -1521,7 +1521,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def wrapper(*args, **kwargs): return func(hass, *args[1:], **kwargs) - return contextfunction(wrapper) + return pass_context(wrapper) self.globals["device_entities"] = hassfunction(device_entities) self.filters["device_entities"] = pass_context(self.globals["device_entities"]) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc4bb9d72ac..7e4e57f608f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.19.1 +async-upnp-client==0.20.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 @@ -14,10 +14,9 @@ certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.3.2 defusedxml==0.7.1 -distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.44.0 -home-assistant-frontend==20210804.0 +hass-nabucasa==0.46.0 +home-assistant-frontend==20210818.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 @@ -33,7 +32,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.2 +zeroconf==0.36.0 pycryptodome>=3.6.6 @@ -51,6 +50,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/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index b31fc718173..69ca1d6083b 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -15,10 +15,10 @@ from homeassistant.config import get_default_config_dir from homeassistant.requirements import pip_kwargs from homeassistant.util.package import install_package, is_installed, is_virtual_env -# mypy: allow-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, disallow-any-generics, no-warn-return-any -def run(args: list) -> int: +def run(args: list[str]) -> int: """Run a script.""" scripts = [] path = os.path.dirname(__file__) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 07bbaa22954..95bb29c4b9d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -16,10 +16,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, ) +from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util, ensure_unique_string +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" @@ -39,14 +42,17 @@ BASE_PLATFORMS = { "lock", "media_player", "notify", + "number", "remote", "scene", "select", "sensor", + "siren", "switch", "tts", "vacuum", "water_heater", + "weather", } DATA_SETUP_DONE = "setup_done" @@ -419,7 +425,7 @@ def _async_when_setup( hass.async_create_task(when_setup()) return - listeners: list[Callable] = [] + listeners: list[CALLBACK_TYPE] = [] async def _matched_event(event: core.Event) -> None: """Call the callback when we matched an event.""" @@ -440,7 +446,7 @@ def _async_when_setup( @core.callback -def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: +def async_get_loaded_integrations(hass: core.HomeAssistant) -> set[str]: """Return the complete list of loaded integrations.""" integrations = set() for component in hass.config.components: @@ -454,7 +460,9 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: @contextlib.contextmanager -def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator: +def async_start_setup( + hass: core.HomeAssistant, components: Iterable[str] +) -> Generator[None, None, None]: """Keep track of when setup starts and finishes.""" setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) started = dt_util.utcnow() diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 47144f0e782..c81beddb07a 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -6,6 +6,8 @@ import math import attr +# mypy: disallow-any-generics + # Official CSS3 colors from w3.org: # https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 # names do not have spaces in them so that we can compare against @@ -392,7 +394,9 @@ def color_hs_to_xy( return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) -def _match_max_scale(input_colors: tuple, output_colors: tuple) -> tuple: +def _match_max_scale( + input_colors: tuple[int, ...], output_colors: tuple[int, ...] +) -> tuple[int, ...]: """Match the maximum value of the output to the input.""" max_in = max(input_colors) max_out = max(output_colors) 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, diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index f4a02dbe82e..84a3faa0951 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -6,6 +6,8 @@ from numbers import Number from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_GALLONS, VOLUME_LITERS, @@ -17,19 +19,31 @@ VALID_UNITS: tuple[str, ...] = ( VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, + VOLUME_CUBIC_METERS, + VOLUME_CUBIC_FEET, ) -def __liter_to_gallon(liter: float) -> float: +def liter_to_gallon(liter: float) -> float: """Convert a volume measurement in Liter to Gallon.""" return liter * 0.2642 -def __gallon_to_liter(gallon: float) -> float: +def gallon_to_liter(gallon: float) -> float: """Convert a volume measurement in Gallon to Liter.""" return gallon * 3.785 +def cubic_meter_to_cubic_feet(cubic_meter: float) -> float: + """Convert a volume measurement in cubic meter to cubic feet.""" + return cubic_meter * 35.3146667 + + +def cubic_feet_to_cubic_meter(cubic_feet: float) -> float: + """Convert a volume measurement in cubic feet to cubic meter.""" + return cubic_feet * 0.0283168466 + + def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a temperature from one unit to another.""" if from_unit not in VALID_UNITS: @@ -45,8 +59,12 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: result: float = volume if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: - result = __liter_to_gallon(volume) + result = liter_to_gallon(volume) elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: - result = __gallon_to_liter(volume) + result = gallon_to_liter(volume) + elif from_unit == VOLUME_CUBIC_METERS and to_unit == VOLUME_CUBIC_FEET: + result = cubic_meter_to_cubic_feet(volume) + elif from_unit == VOLUME_CUBIC_FEET and to_unit == VOLUME_CUBIC_METERS: + result = cubic_feet_to_cubic_meter(volume) return result diff --git a/mypy.ini b/mypy.ini index 7f0a932f0af..cb6adf5d62e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -704,6 +704,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.neato.*] +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.nest.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1260,15 +1271,9 @@ ignore_errors = true [mypy-homeassistant.components.aemet.*] ignore_errors = true -[mypy-homeassistant.components.alexa.*] -ignore_errors = true - [mypy-homeassistant.components.almond.*] ignore_errors = true -[mypy-homeassistant.components.amcrest.*] -ignore_errors = true - [mypy-homeassistant.components.analytics.*] ignore_errors = true @@ -1326,9 +1331,6 @@ ignore_errors = true [mypy-homeassistant.components.elkm1.*] ignore_errors = true -[mypy-homeassistant.components.emonitor.*] -ignore_errors = true - [mypy-homeassistant.components.enphase_envoy.*] ignore_errors = true @@ -1338,9 +1340,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.*] ignore_errors = true -[mypy-homeassistant.components.filter.*] -ignore_errors = true - [mypy-homeassistant.components.fireservicerota.*] ignore_errors = true @@ -1368,12 +1367,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.*] ignore_errors = true -[mypy-homeassistant.components.google_maps.*] -ignore_errors = true - -[mypy-homeassistant.components.google_pubsub.*] -ignore_errors = true - [mypy-homeassistant.components.gpmdp.*] ignore_errors = true @@ -1470,9 +1463,6 @@ ignore_errors = true [mypy-homeassistant.components.kulersky.*] ignore_errors = true -[mypy-homeassistant.components.lifx.*] -ignore_errors = true - [mypy-homeassistant.components.litejet.*] ignore_errors = true @@ -1515,18 +1505,12 @@ ignore_errors = true [mypy-homeassistant.components.mullvad.*] ignore_errors = true -[mypy-homeassistant.components.neato.*] -ignore_errors = true - [mypy-homeassistant.components.ness_alarm.*] ignore_errors = true [mypy-homeassistant.components.nest.legacy.*] ignore_errors = true -[mypy-homeassistant.components.netio.*] -ignore_errors = true - [mypy-homeassistant.components.nightscout.*] ignore_errors = true @@ -1566,15 +1550,9 @@ ignore_errors = true [mypy-homeassistant.components.ozw.*] ignore_errors = true -[mypy-homeassistant.components.panasonic_viera.*] -ignore_errors = true - [mypy-homeassistant.components.philips_js.*] ignore_errors = true -[mypy-homeassistant.components.pilight.*] -ignore_errors = true - [mypy-homeassistant.components.ping.*] ignore_errors = true @@ -1599,9 +1577,6 @@ ignore_errors = true [mypy-homeassistant.components.profiler.*] ignore_errors = true -[mypy-homeassistant.components.proxmoxve.*] -ignore_errors = true - [mypy-homeassistant.components.rachio.*] ignore_errors = true @@ -1614,9 +1589,6 @@ ignore_errors = true [mypy-homeassistant.components.ruckus_unleashed.*] ignore_errors = true -[mypy-homeassistant.components.sabnzbd.*] -ignore_errors = true - [mypy-homeassistant.components.screenlogic.*] ignore_errors = true @@ -1626,18 +1598,12 @@ ignore_errors = true [mypy-homeassistant.components.sense.*] ignore_errors = true -[mypy-homeassistant.components.sesame.*] -ignore_errors = true - [mypy-homeassistant.components.sharkiq.*] ignore_errors = true [mypy-homeassistant.components.sma.*] ignore_errors = true -[mypy-homeassistant.components.smart_meter_texas.*] -ignore_errors = true - [mypy-homeassistant.components.smartthings.*] ignore_errors = true @@ -1650,9 +1616,6 @@ ignore_errors = true [mypy-homeassistant.components.solaredge.*] ignore_errors = true -[mypy-homeassistant.components.solarlog.*] -ignore_errors = true - [mypy-homeassistant.components.somfy.*] ignore_errors = true @@ -1710,9 +1673,6 @@ ignore_errors = true [mypy-homeassistant.components.tplink.*] ignore_errors = true -[mypy-homeassistant.components.tradfri.*] -ignore_errors = true - [mypy-homeassistant.components.tuya.*] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 187351dace9..d5adf797311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -57,7 +57,7 @@ PySocks==1.7.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 -# homeassistant.components.homekit +# homeassistant.components.camera PyTurboJPEG==1.5.0 # homeassistant.components.vicare @@ -106,10 +106,10 @@ 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 +adb-shell[async]==0.4.0 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -139,7 +139,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.5 +aioambient==1.2.6 # homeassistant.components.asuswrt aioasuswrt==1.3.4 @@ -246,7 +246,7 @@ aioswitcher==2.0.4 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.1 +aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==26 @@ -257,6 +257,9 @@ aioymaps==1.1.0 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airtouch4 +airtouch4pyapi==1.0.5 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 @@ -311,7 +314,8 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.1 +# homeassistant.components.yeelight +async-upnp-client==0.20.0 # homeassistant.components.supla asyncpysupla==0.0.5 @@ -368,7 +372,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.19 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -486,7 +490,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.0 +debugpy==1.4.1 # homeassistant.components.decora # decora==0.6 @@ -518,9 +522,6 @@ discogs_client==2.3.0 # homeassistant.components.discord discord.py==1.7.2 -# homeassistant.components.updater -distro==1.5.0 - # homeassistant.components.digitalloggers dlipower==0.7.165 @@ -531,7 +532,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.29 +dsmr_parser==0.30 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.4 @@ -708,7 +709,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.5 +google-nest-sdm==0.3.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -744,7 +745,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.4 +ha-philipsjs==2.7.5 # homeassistant.components.habitica habitipy==0.2.0 @@ -753,7 +754,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.46.0 # homeassistant.components.splunk hass_splunk==0.1.1 @@ -786,7 +787,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210818.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -840,6 +841,7 @@ ibmiotf==0.3.4 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.iglo @@ -941,6 +943,9 @@ lw12==0.9.2 # homeassistant.components.lyft lyft_rides==0.2 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -975,7 +980,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 @@ -1019,6 +1024,9 @@ netdata==0.2.0 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1041,7 +1049,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 @@ -1257,7 +1265,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 @@ -1275,7 +1283,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 @@ -1342,10 +1350,10 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.21 +pybotvac==0.0.22 # homeassistant.components.nissan_leaf -pycarwings2==2.10 +pycarwings2==2.11 # homeassistant.components.cloudflare pycfdns==1.2.1 @@ -1437,9 +1445,6 @@ pyfido==2.1.1 # homeassistant.components.fireservicerota pyfireservicerota==0.0.43 -# homeassistant.components.flexit -pyflexit==0.3 - # homeassistant.components.flic pyflic==2.0.3 @@ -1462,7 +1467,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.5.3 +pyfronius==0.5.5 # homeassistant.components.ifttt pyfttt==0.3 @@ -1490,7 +1495,7 @@ pyhik==0.2.8 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.73 +pyhomematic==0.1.74 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1604,7 +1609,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.2 +pymodbus==2.5.3rc1 # homeassistant.components.monoprice pymonoprice==0.3 @@ -1613,7 +1618,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 @@ -1652,7 +1657,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 @@ -1806,7 +1811,7 @@ pysyncthru==0.7.3 pytankerkoenig==0.0.6 # homeassistant.components.tautulli -pytautulli==0.5.0 +pytautulli==21.8.1 # homeassistant.components.tfiac pytfiac==0.4 @@ -1863,7 +1868,7 @@ python-juicenet==1.0.2 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.6 +python-miio==0.5.7 # homeassistant.components.mpd python-mpd2==3.0.4 @@ -1874,9 +1879,6 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 -# homeassistant.components.nmap_tracker -python-nmap==0.6.1 - # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 @@ -1978,7 +1980,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.6 +pywemo==0.6.7 # homeassistant.components.wilight pywilight==0.0.70 @@ -1993,7 +1995,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 @@ -2096,7 +2098,7 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.0 +sense_energy==0.9.2 # homeassistant.components.sentry sentry-sdk==1.3.0 @@ -2117,7 +2119,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.3 +simplisafe-python==11.0.4 # homeassistant.components.sisyphus sisyphus-control==3.0 @@ -2155,7 +2157,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 @@ -2164,7 +2166,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.6 +solax==0.2.8 # homeassistant.components.honeywell somecomfort==0.5.2 @@ -2424,13 +2426,13 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.10 +youless-api==0.12 # homeassistant.components.media_extractor youtube_dl==2021.04.26 @@ -2442,7 +2444,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.36.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test.txt b/requirements_test.txt index aceec3229a9..63e102ec77e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,14 +2,17 @@ # make new things fail. Manually update these pins when pulling in a # new version +# types-* that have versions roughly corresponding to the packages they +# contain hints for available should be kept in sync with them + -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -codecov==2.1.11 +codecov==2.1.12 coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.902 -pre-commit==2.13.0 +mypy==0.910 +pre-commit==2.14.0 pylint==2.9.5 pipdeptree==1.0.0 pylint-strict-informational==0.1 @@ -25,19 +28,19 @@ responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 tqdm==4.49.0 -types-backports==0.1.2 -types-certifi==0.1.3 -types-chardet==0.1.2 +types-backports==0.1.3 +types-certifi==0.1.4 +types-chardet==0.1.5 types-cryptography==3.3.2 -types-decorator==0.1.4 -types-emoji==1.2.1 -types-enum34==0.1.5 -types-ipaddress==0.1.2 +types-decorator==0.1.7 +types-emoji==1.2.4 +types-enum34==0.1.8 +types-ipaddress==0.1.5 types-jwt==0.1.3 -types-pkg-resources==0.1.2 -types-python-slugify==0.1.0 -types-pytz==0.1.1 -types-PyYAML==5.4.1 -types-requests==0.1.11 -types-toml==0.1.2 -types-ujson==0.1.0 +types-pkg-resources==0.1.3 +types-python-slugify==0.1.2 +types-pytz==2021.1.2 +types-PyYAML==5.4.6 +types-requests==2.25.1 +types-toml==0.1.5 +types-ujson==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e673479b616..ef1e3ca0fc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -26,7 +26,7 @@ PyRMVtransport==0.3.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 -# homeassistant.components.homekit +# homeassistant.components.camera PyTurboJPEG==1.5.0 # homeassistant.components.xiaomi_aqara @@ -48,10 +48,10 @@ 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 +adb-shell[async]==0.4.0 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -78,7 +78,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.5 +aioambient==1.2.6 # homeassistant.components.asuswrt aioasuswrt==1.3.4 @@ -167,7 +167,7 @@ aioswitcher==2.0.4 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.1 +aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==26 @@ -178,6 +178,9 @@ aioymaps==1.1.0 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airtouch4 +airtouch4pyapi==1.0.5 + # homeassistant.components.ambee ambee==0.3.0 @@ -202,7 +205,8 @@ arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.1 +# homeassistant.components.yeelight +async-upnp-client==0.20.0 # homeassistant.components.aurora auroranoaa==0.0.2 @@ -223,7 +227,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.19 # homeassistant.components.blebox blebox_uniapi==1.3.3 @@ -282,7 +286,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.0 +debugpy==1.4.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -299,14 +303,11 @@ devolo-home-control-api==0.17.4 # homeassistant.components.directv directv==0.4.0 -# homeassistant.components.updater -distro==1.5.0 - # homeassistant.components.doorbird doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.29 +dsmr_parser==0.30 # homeassistant.components.dynalite dynalite_devices==0.1.46 @@ -404,7 +405,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.5 +google-nest-sdm==0.3.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -422,7 +423,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.4 +ha-philipsjs==2.7.5 # homeassistant.components.habitica habitipy==0.2.0 @@ -431,7 +432,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.46.0 # homeassistant.components.tasmota hatasmota==0.2.20 @@ -452,7 +453,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210818.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -486,6 +487,7 @@ iaqualink==0.3.90 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.influxdb @@ -527,6 +529,9 @@ logi_circle==0.2.2 # homeassistant.components.luftdaten luftdaten==0.6.5 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -546,7 +551,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 @@ -575,6 +580,9 @@ nessclient==0.9.15 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -582,7 +590,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 @@ -705,7 +713,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 @@ -717,7 +725,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 @@ -760,7 +768,7 @@ pyatv==0.8.2 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.21 +pybotvac==0.0.22 # homeassistant.components.cloudflare pycfdns==1.2.1 @@ -839,7 +847,7 @@ pyheos==0.7.2 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.73 +pyhomematic==0.1.74 # homeassistant.components.ialarm pyialarm==1.9.0 @@ -914,13 +922,13 @@ pymfy==0.11.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.2 +pymodbus==2.5.3rc1 # homeassistant.components.monoprice pymonoprice==0.3 # homeassistant.components.myq -pymyq==3.0.4 +pymyq==3.1.2 # homeassistant.components.mysensors pymysensors==0.21.0 @@ -941,7 +949,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 @@ -1041,7 +1049,7 @@ python-izone==1.1.6 python-juicenet==1.0.2 # homeassistant.components.xiaomi_miio -python-miio==0.5.6 +python-miio==0.5.7 # homeassistant.components.nest python-nest==4.1.0 @@ -1098,7 +1106,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.6 +pywemo==0.6.7 # homeassistant.components.wilight pywilight==0.0.70 @@ -1153,7 +1161,7 @@ screenlogicpy==0.4.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.0 +sense_energy==0.9.2 # homeassistant.components.sentry sentry-sdk==1.3.0 @@ -1165,7 +1173,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.3 +simplisafe-python==11.0.4 # homeassistant.components.slack slackclient==2.5.0 @@ -1183,7 +1191,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 @@ -1338,16 +1346,16 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.youless -youless-api==0.10 +youless-api==0.12 # homeassistant.components.onvif zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.36.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 795a4c3bcd6..e89785c25a8 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,10 +7,10 @@ flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.2 -isort==5.8.0 +isort==5.9.3 mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.23.0 +pyupgrade==2.23.3 yamllint==1.26.1 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 diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index e4d1be7bc46..8e0f53fd736 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -29,31 +29,6 @@ def validate_integration(config: Config, integration: Integration): "config_flow", "Config flows need to be defined in the file config_flow.py", ) - if integration.manifest.get("homekit"): - integration.add_error( - "config_flow", - "HomeKit information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("mqtt"): - integration.add_error( - "config_flow", - "MQTT information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("ssdp"): - integration.add_error( - "config_flow", - "SSDP information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("zeroconf"): - integration.add_error( - "config_flow", - "Zeroconf information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("dhcp"): - integration.add_error( - "config_flow", - "DHCP information in a manifest requires a config flow to exist", - ) return config_flow = config_flow_file.read_text() @@ -98,17 +73,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: - continue - - if not ( - integration.manifest.get("config_flow") - or integration.manifest.get("homekit") - or integration.manifest.get("mqtt") - or integration.manifest.get("ssdp") - or integration.manifest.get("zeroconf") - or integration.manifest.get("dhcp") - ): + if not integration.manifest or not integration.config_flow: continue validate_integration(config, integration) diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index a3abe80063e..c746c64e46f 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -24,7 +24,7 @@ def generate_and_validate(integrations: list[dict[str, str]]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue match_types = integration.manifest.get("dhcp", []) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index b20df6ea42f..69810686cc1 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -96,6 +96,11 @@ class Integration: """Return quality scale of the integration.""" return self.manifest.get("quality_scale") + @property + def config_flow(self) -> str: + """Return if the integration has a config flow.""" + return self.manifest.get("config_flow") + @property def requirements(self) -> list[str]: """List of requirements.""" diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index 718df4ac827..f325518d7b9 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -26,7 +26,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue mqtt = integration.manifest.get("mqtt") diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 23967721053..73081ddfc53 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,9 +16,7 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", - "homeassistant.components.alexa.*", "homeassistant.components.almond.*", - "homeassistant.components.amcrest.*", "homeassistant.components.analytics.*", "homeassistant.components.asuswrt.*", "homeassistant.components.atag.*", @@ -38,11 +36,9 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", "homeassistant.components.elkm1.*", - "homeassistant.components.emonitor.*", "homeassistant.components.enphase_envoy.*", "homeassistant.components.entur_public_transport.*", "homeassistant.components.evohome.*", - "homeassistant.components.filter.*", "homeassistant.components.fireservicerota.*", "homeassistant.components.firmata.*", "homeassistant.components.flo.*", @@ -52,8 +48,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", "homeassistant.components.google_assistant.*", - "homeassistant.components.google_maps.*", - "homeassistant.components.google_pubsub.*", "homeassistant.components.gpmdp.*", "homeassistant.components.gree.*", "homeassistant.components.growatt_server.*", @@ -86,7 +80,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.konnected.*", "homeassistant.components.kostal_plenticore.*", "homeassistant.components.kulersky.*", - "homeassistant.components.lifx.*", "homeassistant.components.litejet.*", "homeassistant.components.litterrobot.*", "homeassistant.components.lovelace.*", @@ -101,10 +94,8 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.mobile_app.*", "homeassistant.components.motion_blinds.*", "homeassistant.components.mullvad.*", - "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.legacy.*", - "homeassistant.components.netio.*", "homeassistant.components.nightscout.*", "homeassistant.components.nilu.*", "homeassistant.components.nmap_tracker.*", @@ -118,9 +109,7 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.*", "homeassistant.components.ovo_energy.*", "homeassistant.components.ozw.*", - "homeassistant.components.panasonic_viera.*", "homeassistant.components.philips_js.*", - "homeassistant.components.pilight.*", "homeassistant.components.ping.*", "homeassistant.components.pioneer.*", "homeassistant.components.plaato.*", @@ -129,24 +118,19 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.plum_lightpad.*", "homeassistant.components.point.*", "homeassistant.components.profiler.*", - "homeassistant.components.proxmoxve.*", "homeassistant.components.rachio.*", "homeassistant.components.ring.*", "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", - "homeassistant.components.sabnzbd.*", "homeassistant.components.screenlogic.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", - "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", "homeassistant.components.sma.*", - "homeassistant.components.smart_meter_texas.*", "homeassistant.components.smartthings.*", "homeassistant.components.smarttub.*", "homeassistant.components.smarty.*", "homeassistant.components.solaredge.*", - "homeassistant.components.solarlog.*", "homeassistant.components.somfy.*", "homeassistant.components.somfy_mylink.*", "homeassistant.components.sonarr.*", @@ -166,7 +150,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.todoist.*", "homeassistant.components.toon.*", "homeassistant.components.tplink.*", - "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", "homeassistant.components.unifi.*", "homeassistant.components.upnp.*", @@ -207,7 +190,9 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { } # This is basically the list of checks which is enabled for "strict=true". -# But "strict=true" is applied globally, so we need to list all checks manually. +# "strict=false" in config files does not turn strict settings off if they've been +# set in a more general section (it instead means as if strict was not specified at +# all), so we need to list all checks manually to be able to flip them wholesale. STRICT_SETTINGS: Final[list[str]] = [ "check_untyped_defs", "disallow_incomplete_defs", diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index c71d5432adf..0611f9a2225 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -31,7 +31,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue ssdp = integration.manifest.get("ssdp") diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 907c6aaceff..4ce4896952e 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -28,7 +28,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue service_types = integration.manifest.get("zeroconf", []) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index f597ef609ea..8b1bdc93749 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -1,8 +1,6 @@ """The NEW_NAME integration.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -13,6 +11,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -34,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NEW_NAME component.""" hass.data[DOMAIN] = {} diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index c1f34d5f5b1..e30cd400bf2 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,17 +1,16 @@ """The NEW_NAME integration.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NEW_NAME integration.""" return True diff --git a/tests/common.py b/tests/common.py index 5de58a08472..3d5e28be514 100644 --- a/tests/common.py +++ b/tests/common.py @@ -29,10 +29,9 @@ from homeassistant.auth import ( providers as auth_providers, ) from homeassistant.auth.permissions import system_policies -from homeassistant.components import recorder +from homeassistant.components import device_automation, recorder from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, - _async_get_device_automations as async_get_device_automations, ) from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config import async_process_component_config @@ -69,6 +68,16 @@ CLIENT_ID = "https://example.com/app" CLIENT_REDIRECT_URI = "https://example.com/app/callback" +async def async_get_device_automations( + hass: HomeAssistant, automation_type: str, device_id: str +) -> Any: + """Get a device automation for a single device id.""" + automations = await device_automation.async_get_device_automations( + hass, automation_type, [device_id] + ) + return automations.get(device_id) + + def threadsafe_callback_factory(func): """Create threadsafe functions out of callbacks. diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index c566702a5b4..cd17c692176 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -7,10 +7,13 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -38,6 +41,7 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "23" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AQI entry = registry.async_get("sensor.home_caqi") assert entry @@ -63,7 +67,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM1 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm1") @@ -78,7 +82,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm2_5") @@ -93,7 +97,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm10") diff --git a/tests/components/airtouch4/__init__.py b/tests/components/airtouch4/__init__.py new file mode 100644 index 00000000000..cc267ee41d1 --- /dev/null +++ b/tests/components/airtouch4/__init__.py @@ -0,0 +1 @@ +"""Tests for the AirTouch4 integration.""" diff --git a/tests/components/airtouch4/test_config_flow.py b/tests/components/airtouch4/test_config_flow.py new file mode 100644 index 00000000000..a98b24ef88d --- /dev/null +++ b/tests/components/airtouch4/test_config_flow.py @@ -0,0 +1,123 @@ +"""Test the AirTouch 4 config flow.""" +from unittest.mock import AsyncMock, Mock, patch + +from airtouch4pyapi.airtouch import AirTouch, AirTouchAc, AirTouchGroup, AirTouchStatus + +from homeassistant import config_entries +from homeassistant.components.airtouch4.const import DOMAIN + + +async def test_form(hass): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + mock_ac = AirTouchAc() + mock_groups = AirTouchGroup() + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.Status = AirTouchStatus.OK + mock_airtouch.GetAcs = Mock(return_value=[mock_ac]) + mock_airtouch.GetGroups = Mock(return_value=[mock_groups]) + + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ), patch( + "homeassistant.components.airtouch4.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "0.0.0.1" + assert result2["data"] == { + "host": "0.0.0.1", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.CONNECTION_INTERRUPTED + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_library_error_message(hass): + """Test we handle an unknown error message from the library.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.ERROR + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.NOT_CONNECTED + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_ac = AirTouchAc() + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.Status = AirTouchStatus.OK + mock_airtouch.GetAcs = Mock(return_value=[mock_ac]) + mock_airtouch.GetGroups = Mock(return_value=[]) + + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index b8389d1903f..2c1f3e26b54 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -5,8 +5,10 @@ from unittest.mock import patch import pytest from homeassistant.components.arlo import DATA_ARLO, sensor as arlo +from homeassistant.components.arlo.sensor import SENSOR_TYPES from homeassistant.const import ( ATTR_ATTRIBUTION, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -17,49 +19,55 @@ def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) -def _get_sensor(name="Last", sensor_type="last_capture", data=None): +def _get_sensor(hass, name="Last", sensor_type="last_capture", data=None): if data is None: data = {} - return arlo.ArloSensor(name, data, sensor_type) + sensor_entry = next( + sensor_entry for sensor_entry in SENSOR_TYPES if sensor_entry.key == sensor_type + ) + sensor_entry.name = name + sensor = arlo.ArloSensor(data, sensor_entry) + sensor.hass = hass + return sensor @pytest.fixture() -def default_sensor(): +def default_sensor(hass): """Create an ArloSensor with default values.""" - return _get_sensor() + return _get_sensor(hass) @pytest.fixture() -def battery_sensor(): +def battery_sensor(hass): """Create an ArloSensor with battery data.""" data = _get_named_tuple({"battery_level": 50}) - return _get_sensor("Battery Level", "battery_level", data) + return _get_sensor(hass, "Battery Level", "battery_level", data) @pytest.fixture() -def temperature_sensor(): +def temperature_sensor(hass): """Create a temperature ArloSensor.""" - return _get_sensor("Temperature", "temperature") + return _get_sensor(hass, "Temperature", "temperature") @pytest.fixture() -def humidity_sensor(): +def humidity_sensor(hass): """Create a humidity ArloSensor.""" - return _get_sensor("Humidity", "humidity") + return _get_sensor(hass, "Humidity", "humidity") @pytest.fixture() -def cameras_sensor(): +def cameras_sensor(hass): """Create a total cameras ArloSensor.""" data = _get_named_tuple({"cameras": [0, 0]}) - return _get_sensor("Arlo Cameras", "total_cameras", data) + return _get_sensor(hass, "Arlo Cameras", "total_cameras", data) @pytest.fixture() -def captured_sensor(): +def captured_sensor(hass): """Create a captured today ArloSensor.""" data = _get_named_tuple({"captured_today": [0, 0, 0, 0, 0]}) - return _get_sensor("Captured Today", "captured_today", data) + return _get_sensor(hass, "Captured Today", "captured_today", data) class PlatformSetupFixture: @@ -82,14 +90,6 @@ def platform_setup(): return PlatformSetupFixture() -@pytest.fixture() -def sensor_with_hass_data(default_sensor, hass): - """Create a sensor with async_dispatcher_connected mocked.""" - hass.data = {} - default_sensor.hass = hass - return default_sensor - - @pytest.fixture() def mock_dispatch(): """Mock the dispatcher connect method.""" @@ -139,14 +139,14 @@ def test_sensor_name(default_sensor): assert default_sensor.name == "Last" -async def test_async_added_to_hass(sensor_with_hass_data, mock_dispatch): +async def test_async_added_to_hass(default_sensor, mock_dispatch): """Test dispatcher called when added.""" - await sensor_with_hass_data.async_added_to_hass() + await default_sensor.async_added_to_hass() assert len(mock_dispatch.mock_calls) == 1 kall = mock_dispatch.call_args args, kwargs = kall assert len(args) == 3 - assert args[0] == sensor_with_hass_data.hass + assert args[0] == default_sensor.hass assert args[1] == "arlo_update" assert not kwargs @@ -156,14 +156,14 @@ def test_sensor_state_default(default_sensor): assert default_sensor.state is None -def test_sensor_icon_battery(battery_sensor): - """Test the battery icon.""" - assert battery_sensor.icon == "mdi:battery-50" +def test_sensor_device_class__battery(battery_sensor): + """Test the battery device_class.""" + assert battery_sensor.device_class == DEVICE_CLASS_BATTERY -def test_sensor_icon(temperature_sensor): - """Test the icon property.""" - assert temperature_sensor.icon == "mdi:thermometer" +def test_sensor_device_class(temperature_sensor): + """Test the device_class property.""" + assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE def test_unit_of_measure(default_sensor, battery_sensor): @@ -191,22 +191,22 @@ def test_update_captured_today(captured_sensor): assert captured_sensor.state == 5 -def _test_attributes(sensor_type): +def _test_attributes(hass, sensor_type): data = _get_named_tuple({"model_id": "TEST123"}) - sensor = _get_sensor("test", sensor_type, data) + sensor = _get_sensor(hass, "test", sensor_type, data) attrs = sensor.extra_state_attributes assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com" assert attrs.get("brand") == "Netgear Arlo" assert attrs.get("model") == "TEST123" -def test_state_attributes(): +def test_state_attributes(hass): """Test attributes for camera sensor types.""" - _test_attributes("battery_level") - _test_attributes("signal_strength") - _test_attributes("temperature") - _test_attributes("humidity") - _test_attributes("air_quality") + _test_attributes(hass, "battery_level") + _test_attributes(hass, "signal_strength") + _test_attributes(hass, "temperature") + _test_attributes(hass, "humidity") + _test_attributes(hass, "air_quality") def test_attributes_total_cameras(cameras_sensor): @@ -217,17 +217,17 @@ def test_attributes_total_cameras(cameras_sensor): assert attrs.get("model") is None -def _test_update(sensor_type, key, value): +def _test_update(hass, sensor_type, key, value): data = _get_named_tuple({key: value}) - sensor = _get_sensor("test", sensor_type, data) + sensor = _get_sensor(hass, "test", sensor_type, data) sensor.update() assert sensor.state == value -def test_update(): +def test_update(hass): """Test update method for direct transcription sensor types.""" - _test_update("battery_level", "battery_level", 100) - _test_update("signal_strength", "signal_strength", 100) - _test_update("temperature", "ambient_temperature", 21.4) - _test_update("humidity", "ambient_humidity", 45.1) - _test_update("air_quality", "ambient_air_quality", 14.2) + _test_update(hass, "battery_level", "battery_level", 100) + _test_update(hass, "signal_strength", "signal_strength", 100) + _test_update(hass, "temperature", "ambient_temperature", 21.4) + _test_update(hass, "humidity", "ambient_humidity", 45.1) + _test_update(hass, "air_quality", "ambient_air_quality", 14.2) diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 93e2596e343..756a553f3c7 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -3,8 +3,12 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ +from unittest.mock import Mock + from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM +EMPTY_8_6_JPEG = b"empty_8_6" + def mock_camera_prefs(hass, entity_id, prefs=None): """Fixture for cloud component.""" @@ -13,3 +17,16 @@ def mock_camera_prefs(hass, entity_id, prefs=None): prefs_to_set.update(prefs) hass.data[DATA_CAMERA_PREFS]._prefs[entity_id] = prefs_to_set return prefs_to_set + + +def mock_turbo_jpeg( + first_width=None, second_width=None, first_height=None, second_height=None +): + """Mock a TurboJPEG instance.""" + mocked_turbo_jpeg = Mock() + mocked_turbo_jpeg.decode_header.side_effect = [ + (first_width, first_height, 0, 0), + (second_width, second_height, 0, 0), + ] + mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG + return mocked_turbo_jpeg diff --git a/tests/components/homekit/test_img_util.py b/tests/components/camera/test_img_util.py similarity index 67% rename from tests/components/homekit/test_img_util.py rename to tests/components/camera/test_img_util.py index 45af8e6b1e6..4f32715800e 100644 --- a/tests/components/homekit/test_img_util.py +++ b/tests/components/camera/test_img_util.py @@ -1,8 +1,10 @@ -"""Test HomeKit img_util module.""" +"""Test img_util module.""" from unittest.mock import patch +from turbojpeg import TurboJPEG + from homeassistant.components.camera import Image -from homeassistant.components.homekit.img_util import ( +from homeassistant.components.camera.img_util import ( TurboJPEGSingleton, scale_jpeg_camera_image, ) @@ -12,13 +14,23 @@ from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg EMPTY_16_12_JPEG = b"empty_16_12" +def _clear_turbojpeg_singleton(): + TurboJPEGSingleton.__instance = None + + +def _reset_turbojpeg_singleton(): + TurboJPEGSingleton.__instance = TurboJPEG() + + def test_turbojpeg_singleton(): """Verify the instance always gives back the same.""" + _clear_turbojpeg_singleton() assert TurboJPEGSingleton.instance() == TurboJPEGSingleton.instance() def test_scale_jpeg_camera_image(): """Test we can scale a jpeg image.""" + _clear_turbojpeg_singleton() camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) @@ -27,6 +39,12 @@ def test_scale_jpeg_camera_image(): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) + turbo_jpeg.decode_header.side_effect = OSError + with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + TurboJPEGSingleton() + assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() @@ -44,11 +62,11 @@ def test_scale_jpeg_camera_image(): def test_turbojpeg_load_failure(): """Handle libjpegturbo not being installed.""" - + _clear_turbojpeg_singleton() with patch("turbojpeg.TurboJPEG", side_effect=Exception): TurboJPEGSingleton() assert TurboJPEGSingleton.instance() is False - with patch("turbojpeg.TurboJPEG"): - TurboJPEGSingleton() - assert TurboJPEGSingleton.instance() + _clear_turbojpeg_singleton() + TurboJPEGSingleton() + assert TurboJPEGSingleton.instance() is not None diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7c7890a3e5f..bb3f76e0d1b 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -20,6 +20,8 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg + from tests.components.camera import common @@ -75,6 +77,74 @@ async def test_get_image_from_camera(hass, image_mock_url): assert image.content == b"Test" +async def test_get_image_from_camera_with_width_height(hass, image_mock_url): + """Grab an image from camera entity with width and height.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Test", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera", width=640, height=480 + ) + + assert mock_camera.called + assert image.content == b"Test" + + +async def test_get_image_from_camera_with_width_height_scaled(hass, image_mock_url): + """Grab an image from camera entity with width and height and scale it.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Valid jpeg", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera", width=4, height=3 + ) + + assert mock_camera.called + assert image.content_type == "image/jpg" + assert image.content == EMPTY_8_6_JPEG + + +async def test_get_image_from_camera_not_jpeg(hass, image_mock_url): + """Grab an image from camera entity that we cannot scale.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"png", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera_png", width=4, height=3 + ) + + assert mock_camera.called + assert image.content_type == "image/png" + assert image.content == b"png" + + async def test_get_stream_source_from_camera(hass, mock_camera): """Fetch stream source from camera entity.""" @@ -153,7 +223,7 @@ async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT assert msg["success"] - assert msg["result"]["content_type"] == "image/jpeg" + assert msg["result"]["content_type"] == "image/jpg" assert msg["result"]["content"] == base64.b64encode(b"Test").decode("utf-8") 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: diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 5fcab6605bd..231a5128585 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -6,7 +6,7 @@ from homeassistant.components.coinbase.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN -from .const import GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2, MOCK_ACCOUNTS_RESPONSE +from .const import GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2, MOCK_ACCOUNTS_RESPONSE from tests.common import MockConfigEntry @@ -60,11 +60,11 @@ def mock_get_exchange_rates(): """Return a heavily reduced mock list of exchange rates for testing.""" return { "currency": "USD", - "rates": {GOOD_EXCHNAGE_RATE_2: "0.109", GOOD_EXCHNAGE_RATE: "0.00002"}, + "rates": {GOOD_EXCHANGE_RATE_2: "0.109", GOOD_EXCHANGE_RATE: "0.00002"}, } -async def init_mock_coinbase(hass): +async def init_mock_coinbase(hass, currencies=None, rates=None): """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -72,8 +72,8 @@ async def init_mock_coinbase(hass): title="Test User", data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, options={ - CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [], + CONF_CURRENCIES: currencies or [], + CONF_EXCHANGE_RATES: rates or [], }, ) config_entry.add_to_hass(hass) diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 7d36d0be9a7..082c986aa59 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -3,8 +3,8 @@ GOOD_CURRENCY = "BTC" GOOD_CURRENCY_2 = "USD" GOOD_CURRENCY_3 = "EUR" -GOOD_EXCHNAGE_RATE = "BTC" -GOOD_EXCHNAGE_RATE_2 = "ATOM" +GOOD_EXCHANGE_RATE = "BTC" +GOOD_EXCHANGE_RATE_2 = "ATOM" BAD_CURRENCY = "ETH" BAD_EXCHANGE_RATE = "ETH" @@ -15,6 +15,15 @@ MOCK_ACCOUNTS_RESPONSE = [ "id": "123456789", "name": "BTC Wallet", "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, + "type": "wallet", + }, + { + "balance": {"amount": "100.00", "currency": GOOD_CURRENCY}, + "currency": GOOD_CURRENCY, + "id": "abcdefg", + "name": "BTC Vault", + "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, + "type": "vault", }, { "balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, @@ -22,5 +31,6 @@ MOCK_ACCOUNTS_RESPONSE = [ "id": "987654321", "name": "USD Wallet", "native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, + "type": "fiat", }, ] diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index d153cecc249..fa13648ee71 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -19,7 +19,7 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE +from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE from tests.common import MockConfigEntry @@ -160,7 +160,7 @@ async def test_option_form(hass): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], }, ) assert result2["type"] == "create_entry" diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 36f0ff95472..efb5ba85f73 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.coinbase.const import ( + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, CONF_YAML_API_TOKEN, @@ -22,8 +23,8 @@ from .common import ( from .const import ( GOOD_CURRENCY, GOOD_CURRENCY_2, - GOOD_EXCHNAGE_RATE, - GOOD_EXCHNAGE_RATE_2, + GOOD_EXCHANGE_RATE, + GOOD_EXCHANGE_RATE_2, ) @@ -34,7 +35,7 @@ async def test_setup(hass): CONF_API_KEY: "123456", CONF_YAML_API_TOKEN: "AbCDeF", CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], } } with patch( @@ -54,7 +55,7 @@ async def test_setup(hass): assert entries[0].source == config_entries.SOURCE_IMPORT assert entries[0].options == { CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], } @@ -103,7 +104,7 @@ async def test_option_updates(hass: HomeAssistant): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], }, ) await hass.async_block_till_done() @@ -126,7 +127,7 @@ async def test_option_updates(hass: HomeAssistant): ] assert currencies == [GOOD_CURRENCY, GOOD_CURRENCY_2] - assert rates == [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2] + assert rates == [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2] result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() @@ -134,7 +135,7 @@ async def test_option_updates(hass: HomeAssistant): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], }, ) await hass.async_block_till_done() @@ -157,4 +158,28 @@ async def test_option_updates(hass: HomeAssistant): ] assert currencies == [GOOD_CURRENCY] - assert rates == [GOOD_EXCHNAGE_RATE] + assert rates == [GOOD_EXCHANGE_RATE] + + +async def test_ignore_vaults_wallets(hass: HomeAssistant): + """Test vaults are ignored in wallet sensors.""" + + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass, currencies=[GOOD_CURRENCY]) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + assert len(entities) == 1 + entity = entities[0] + assert API_TYPE_VAULT not in entity.original_name.lower() diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 160e6354b8b..13190ed4b32 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,6 +1,7 @@ """The test for light device automation.""" import pytest +from homeassistant.components import device_automation import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON @@ -372,6 +373,76 @@ async def test_websocket_get_no_condition_capabilities( assert capabilities == expected_capabilities +async def test_async_get_device_automations_single_device_trigger( + hass, device_reg, entity_reg +): + """Test we get can fetch the triggers for a device id.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + result = await device_automation.async_get_device_automations( + hass, "trigger", [device_entry.id] + ) + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_trigger( + hass, device_reg, entity_reg +): + """Test we get can fetch all the triggers when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + result = await device_automation.async_get_device_automations(hass, "trigger") + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_condition( + hass, device_reg, entity_reg +): + """Test we get can fetch all the conditions when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + result = await device_automation.async_get_device_automations(hass, "condition") + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_action( + hass, device_reg, entity_reg +): + """Test we get can fetch all the actions when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + result = await device_automation.async_get_device_automations(hass, "action") + assert device_entry.id in result + assert len(result[device_entry.id]) == 3 + + async def test_websocket_get_trigger_capabilities( hass, hass_ws_client, device_reg, entity_reg ): diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index ab7b3a4d479..9ef6bccfab5 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -7,6 +7,7 @@ from dsmr_parser.obis_references import ( EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS, LUXEMBOURG_EQUIPMENT_IDENTIFIER, + P1_MESSAGE_TIMESTAMP, ) from dsmr_parser.objects import CosemObject import pytest @@ -44,6 +45,7 @@ async def dsmr_connection_send_validate_fixture(hass): protocol.telegram = { EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), } async def connection_factory(*args, **kwargs): @@ -57,6 +59,10 @@ async def dsmr_connection_send_validate_fixture(hass): [{"value": "123456789", "unit": ""}] ), } + if args[1] == "5S": + protocol.telegram = { + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), + } return (transport, protocol) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 006893a81e8..d56cd3f2eb8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.dsmr import DOMAIN, config_flow from tests.common import MockConfigEntry SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} +SERIAL_DATA_SWEDEN = {"serial_id": None, "serial_id_gas": None} def com_port(): @@ -482,6 +483,29 @@ async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): assert result["data"] == {**entry_data, **SERIAL_DATA} +async def test_import_sweden(hass, dsmr_connection_send_validate_fixture): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5S", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == {**entry_data, **SERIAL_DATA_SWEDEN} + + def test_get_serial_by_id_no_dir(): """Test serial by id conversion if there's no /dev/serial/by-id.""" p1 = patch("os.path.isdir", MagicMock(return_value=False)) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 90194eaeb6b..6accf7c40da 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -14,16 +14,17 @@ from unittest.mock import DEFAULT, MagicMock from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN, @@ -104,7 +105,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), } @@ -135,7 +136,6 @@ async def test_default_setup(hass, dsmr_connection_fixture): assert power_consumption.state == STATE_UNKNOWN assert power_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert power_consumption.attributes.get(ATTR_ICON) is None - assert power_consumption.attributes.get(ATTR_LAST_RESET) is None assert power_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert power_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None @@ -157,17 +157,16 @@ async def test_default_setup(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -228,7 +227,7 @@ async def test_v4_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -256,18 +255,17 @@ async def test_v4_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -299,7 +297,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -327,17 +325,16 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -370,7 +367,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( @@ -402,8 +399,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "123.456" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert power_tariff.attributes.get(ATTR_ICON) is None - assert power_tariff.attributes.get(ATTR_LAST_RESET) is not None - assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert ( power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -415,10 +411,10 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -450,7 +446,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): BELGIUM_HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -478,17 +474,16 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "normal" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -537,11 +532,75 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" +async def test_swedish_meter(hass, dsmr_connection_fixture): + """Test if v5 meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, + ) + from dsmr_parser.objects import CosemObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5S", + "precision": 4, + "reconnect_interval": 30, + "serial_id": None, + "serial_id_gas": None, + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + ), + SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + power_tariff = hass.states.get("sensor.energy_consumption_total") + assert power_tariff.state == "123.456" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert power_tariff.attributes.get(ATTR_ICON) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert ( + power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + ) + + power_tariff = hass.states.get("sensor.energy_production_total") + assert power_tariff.state == "654.321" + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert ( + power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + ) + + async def test_tcp(hass, dsmr_connection_fixture): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = dsmr_connection_fixture diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 978b21e1919..ea183ec52f4 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -7,9 +7,8 @@ import pytest from homeassistant.components.energy import data from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.sensor.recorder import compile_statistics from homeassistant.const import ( @@ -18,6 +17,7 @@ from homeassistant.const import ( DEVICE_CLASS_MONETARY, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_METERS, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -130,14 +130,13 @@ async def test_cost_sensor_price_entity( } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() # Optionally initialize dependent entities if initial_energy is not None: hass.states.async_set( usage_sensor_entity_id, initial_energy, - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) hass.states.async_set("sensor.energy_price", "1") @@ -147,9 +146,7 @@ async def test_cost_sensor_price_entity( state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY - if initial_cost != "unknown": - assert state.attributes[ATTR_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -159,7 +156,6 @@ async def test_cost_sensor_price_entity( usage_sensor_entity_id, "0", { - "last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, ) @@ -168,8 +164,7 @@ async def test_cost_sensor_price_entity( state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY - assert state.attributes[ATTR_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -181,7 +176,7 @@ async def test_cost_sensor_price_entity( hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -205,7 +200,7 @@ async def test_cost_sensor_price_entity( hass.states.async_set( usage_sensor_entity_id, "14.5", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -217,32 +212,31 @@ async def test_cost_sensor_price_entity( assert cost_sensor_entity_id in statistics assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0 - # Energy sensor is reset, with start point at 4kWh - last_reset = (now + timedelta(seconds=1)).isoformat() + # Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point hass.states.async_set( usage_sensor_entity_id, "4", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {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 + assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR # Energy use bumped to 10 kWh hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {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 + assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR # Check generated statistics await async_wait_recording_done_without_instance(hass) 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 + assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 39.0 async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: @@ -271,12 +265,11 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: } 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}, + {"unit_of_measurement": ENERGY_WATT_HOUR}, ) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -289,9 +282,54 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: hass.states.async_set( "sensor.energy_consumption", 20000, - {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + {"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" + + +async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: + """Test gas cost price from sensor entity.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption", + "entity_energy_from": "sensor.gas_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 0.5, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + now = dt_util.utcnow() + + hass.states.async_set( + "sensor.gas_consumption", + 100, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, + ) + + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass) + + state = hass.states.get("sensor.gas_consumption_cost") + assert state.state == "0.0" + + # gas use bumped to 10 kWh + hass.states.async_set( + "sensor.gas_consumption", + 200, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.gas_consumption_cost") + assert state.state == "50.0" diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py new file mode 100644 index 00000000000..9a0b2105007 --- /dev/null +++ b/tests/components/energy/test_validate.py @@ -0,0 +1,443 @@ +"""Test that validation works.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.energy import async_get_manager, validate +from homeassistant.setup import async_setup_component + +from tests.common import async_init_recorder_component + + +@pytest.fixture +def mock_is_entity_recorded(): + """Mock recorder.is_entity_recorded.""" + mocks = {} + + with patch( + "homeassistant.components.recorder.is_entity_recorded", + side_effect=lambda hass, entity_id: mocks.get(entity_id, True), + ): + yield mocks + + +@pytest.fixture(autouse=True) +async def mock_energy_manager(hass): + """Set up energy.""" + await async_init_recorder_component(hass) + assert await async_setup_component(hass, "energy", {"energy": {}}) + manager = await async_get_manager(hass) + manager.data = manager.default_preferences() + return manager + + +async def test_validation_empty_config(hass): + """Test validating an empty config.""" + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [], + } + + +async def test_validation(hass, mock_energy_manager): + """Test validating success.""" + for key in ("device_cons", "battery_import", "battery_export", "solar_production"): + hass.states.async_set( + f"sensor.{key}", + "123", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "battery", + "stat_energy_from": "sensor.battery_import", + "stat_energy_to": "sensor.battery_export", + }, + {"type": "solar", "stat_energy_from": "sensor.solar_production"}, + ], + "device_consumption": [{"stat_consumption": "sensor.device_cons"}], + } + ) + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [[], []], + "device_consumption": [[]], + } + + +async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.not_exist"}]} + ) + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_not_defined", + "identifier": "sensor.not_exist", + "value": None, + } + ] + ], + } + + +async def test_validation_device_consumption_entity_unavailable( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.unavailable"}]} + ) + hass.states.async_set("sensor.unavailable", "unavailable", {}) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_unavailable", + "identifier": "sensor.unavailable", + "value": "unavailable", + } + ] + ], + } + + +async def test_validation_device_consumption_entity_non_numeric( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.non_numeric"}]} + ) + hass.states.async_set("sensor.non_numeric", "123,123.10") + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_state_non_numeric", + "identifier": "sensor.non_numeric", + "value": "123,123.10", + }, + ] + ], + } + + +async def test_validation_device_consumption_entity_unexpected_unit( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.unexpected_unit"}]} + ) + hass.states.async_set( + "sensor.unexpected_unit", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.unexpected_unit", + "value": "beers", + } + ] + ], + } + + +async def test_validation_device_consumption_recorder_not_tracked( + hass, mock_energy_manager, mock_is_entity_recorded +): + """Test validating device based on untracked entity.""" + mock_is_entity_recorded["sensor.not_recorded"] = False + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.not_recorded"}]} + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "recorder_untracked", + "identifier": "sensor.not_recorded", + "value": None, + } + ] + ], + } + + +async def test_validation_solar(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + { + "energy_sources": [ + {"type": "solar", "stat_energy_from": "sensor.solar_production"} + ] + } + ) + hass.states.async_set( + "sensor.solar_production", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.solar_production", + "value": "beers", + } + ] + ], + "device_consumption": [], + } + + +async def test_validation_battery(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "battery", + "stat_energy_from": "sensor.battery_import", + "stat_energy_to": "sensor.battery_export", + } + ] + } + ) + hass.states.async_set( + "sensor.battery_import", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.battery_export", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.battery_import", + "value": "beers", + }, + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.battery_export", + "value": "beers", + }, + ] + ], + "device_consumption": [], + } + + +async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorded): + """Test validating grid with sensors for energy and cost/compensation.""" + mock_is_entity_recorded["sensor.grid_cost_1"] = False + mock_is_entity_recorded["sensor.grid_compensation_1"] = False + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "stat_cost": "sensor.grid_cost_1", + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "stat_compensation": "sensor.grid_compensation_1", + } + ], + } + ] + } + ) + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_consumption_1", + "value": "beers", + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_cost_1", + "value": None, + }, + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_production_1", + "value": "beers", + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_compensation_1", + "value": None, + }, + ] + ], + "device_consumption": [], + } + + +async def test_validation_grid_price_not_exist(hass, mock_energy_manager): + """Test validating grid with price entity that does not exist.""" + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "entity_energy_from": "sensor.grid_consumption_1", + "entity_energy_price": "sensor.grid_price_1", + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "entity_energy_to": "sensor.grid_production_1", + "number_energy_price": 0.10, + } + ], + } + ] + } + ) + await hass.async_block_till_done() + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_not_defined", + "identifier": "sensor.grid_price_1", + "value": None, + } + ] + ], + "device_consumption": [], + } + + +@pytest.mark.parametrize( + "state, unit, expected", + ( + ( + "123,123.12", + "$/kWh", + { + "type": "entity_state_non_numeric", + "identifier": "sensor.grid_price_1", + "value": "123,123.12", + }, + ), + ( + "-100", + "$/kWh", + { + "type": "entity_negative_state", + "identifier": "sensor.grid_price_1", + "value": -100.0, + }, + ), + ( + "123", + "$/Ws", + { + "type": "entity_unexpected_unit_price", + "identifier": "sensor.grid_price_1", + "value": "$/Ws", + }, + ), + ), +) +async def test_validation_grid_price_errors( + hass, mock_energy_manager, state, unit, expected +): + """Test validating grid with price data that gives errors.""" + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_price_1", + state, + {"unit_of_measurement": unit, "state_class": "total_increasing"}, + ) + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "entity_energy_from": "sensor.grid_consumption_1", + "entity_energy_price": "sensor.grid_price_1", + } + ], + "flow_to": [], + } + ] + } + ) + await hass.async_block_till_done() + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [expected], + ], + "device_consumption": [], + } diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index a14a8d0986e..732bdaa93cf 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -104,6 +104,11 @@ async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: "stat_energy_from": "my_solar_production", "config_entry_solar_forecast": ["predicted_config_entry"], }, + { + "type": "battery", + "stat_energy_from": "my_battery_draining", + "stat_energy_to": "my_battery_charging", + }, ], "device_consumption": [{"stat_consumption": "some_device_usage"}], } @@ -211,3 +216,19 @@ async def test_handle_duplicate_from_stat(hass, hass_ws_client) -> None: assert msg["id"] == 5 assert not msg["success"] assert msg["error"]["code"] == "invalid_format" + + +async def test_validate(hass, hass_ws_client) -> None: + """Test we can validate the preferences.""" + client = await hass_ws_client(hass) + + await client.send_json({"id": 5, "type": "energy/validate"}) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == { + "energy_sources": [], + "device_consumption": [], + } diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index f9c78e14888..0240ffc6d11 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -83,6 +83,7 @@ async def test_single_ban(hass): """Test that log is parsed correctly for single ban.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("single_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -97,6 +98,7 @@ async def test_ipv6_ban(hass): """Test that log is parsed correctly for IPV6 bans.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("ipv6_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -111,6 +113,7 @@ async def test_multiple_ban(hass): """Test that log is parsed correctly for multiple ban.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("multi_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -131,6 +134,7 @@ async def test_unban_all(hass): """Test that log is parsed correctly when unbanning.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("unban_all")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -148,6 +152,7 @@ async def test_unban_one(hass): """Test that log is parsed correctly when unbanning one ip.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("unban_one")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -166,6 +171,8 @@ async def test_multi_jail(hass): log_parser = BanLogParser("/test/fail2ban.log") sensor1 = BanSensor("fail2ban", "jail_one", log_parser) sensor2 = BanSensor("fail2ban", "jail_two", log_parser) + sensor1.hass = hass + sensor2.hass = hass assert sensor1.name == "fail2ban jail_one" assert sensor2.name == "fail2ban jail_two" mock_fh = mock_open(read_data=fake_log("multi_jail")) @@ -185,6 +192,7 @@ async def test_ban_active_after_update(hass): """Test that ban persists after subsequent update.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("single_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 3c6a2fbb92d..e1730ffdabb 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,7 +1,7 @@ """The tests for Home Assistant ffmpeg.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock, Mock, call, patch -import homeassistant.components.ffmpeg as ffmpeg +from homeassistant.components import ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, SERVICE_RESTART, @@ -181,3 +181,58 @@ async def test_setup_component_test_service_start_with_entity(hass): assert ffmpeg_dev.called_start assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"] + + +async def test_async_get_image_with_width_height(hass): + """Test fetching an image with a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image(hass, "rtsp://fake", width=640, height=480) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 640x480") + ] + + +async def test_async_get_image_with_extra_cmd_overlapping_width_height(hass): + """Test fetching an image with and extra_cmd with width and height and a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image( + hass, "rtsp://fake", extra_cmd="-s 1024x768", width=640, height=480 + ) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 1024x768") + ] + + +async def test_async_get_image_with_extra_cmd_width_height(hass): + """Test fetching an image with and extra_cmd and a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image( + hass, "rtsp://fake", extra_cmd="-vf any", width=640, height=480 + ) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-vf any -s 640x480") + ] diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 951528f1e7d..27461b2790f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -10,10 +10,10 @@ from homeassistant.components.fritzbox.const import ( DOMAIN as FB_DOMAIN, ) from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.switch import DOMAIN from homeassistant.const import ( @@ -73,10 +73,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") assert state assert state.state == "1.234" - 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 state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING async def test_turn_on(hass: HomeAssistant, fritz: Mock): diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 2da3d8e1e8c..adf151f4819 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -18,9 +18,17 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CO, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_SULPHUR_DIOXIDE, STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry as er @@ -45,7 +53,7 @@ async def test_sensor(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_c6h6") @@ -57,12 +65,12 @@ async def test_sensor(hass): assert state.state == "252" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_co") @@ -74,12 +82,12 @@ async def test_sensor(hass): assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_NITROGEN_DIOXIDE assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_no2") @@ -91,12 +99,12 @@ async def test_sensor(hass): assert state.state == "96" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OZONE assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_o3") @@ -108,12 +116,12 @@ async def test_sensor(hass): assert state.state == "17" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm10") @@ -125,12 +133,12 @@ async def test_sensor(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm2_5") @@ -142,12 +150,12 @@ async def test_sensor(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SULPHUR_DIOXIDE assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_so2") @@ -159,9 +167,9 @@ async def test_sensor(hass): assert state.state == "dobry" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AQI assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get("sensor.home_aqi") assert entry @@ -225,7 +233,7 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_c6h6") @@ -242,7 +250,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_co") @@ -259,7 +266,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_no2") @@ -276,7 +282,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_o3") @@ -293,7 +298,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_pm10") @@ -310,7 +314,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_pm2_5") @@ -327,7 +330,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_so2") diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index fc1fecb04ed..d31d28e7302 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,6 +1,7 @@ """The tests for the Google Pub/Sub component.""" from dataclasses import dataclass from datetime import datetime +import os import unittest.mock as mock import pytest @@ -51,13 +52,12 @@ def mock_client_fixture(): yield client -@pytest.fixture(autouse=True, name="mock_os") -def mock_os_fixture(): - """Mock the OS cli.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.os") as os_cli: - os_cli.path = mock.MagicMock() - setattr(os_cli.path, "join", mock.MagicMock(return_value="path")) - yield os_cli +@pytest.fixture(autouse=True, name="mock_is_file") +def mock_is_file_fixture(): + """Mock os.path.isfile.""" + with mock.patch(f"{GOOGLE_PUBSUB_PATH}.os.path.isfile") as is_file: + is_file.return_value = True + yield is_file @pytest.fixture(autouse=True) @@ -84,9 +84,9 @@ async def test_minimal_config(hass, mock_client): assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert ( - mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path" - ) + assert mock_client.PublisherClient.from_service_account_json.call_args[0][ + 0 + ] == os.path.join(hass.config.config_dir, "creds") async def test_full_config(hass, mock_client): @@ -111,9 +111,9 @@ async def test_full_config(hass, mock_client): assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert ( - mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path" - ) + assert mock_client.PublisherClient.from_service_account_json.call_args[0][ + 0 + ] == os.path.join(hass.config.config_dir, "creds") def make_event(entity_id): diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 6f4b4652e76..9b430fa5fae 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -68,7 +68,7 @@ async def test_setup_get(hass, requests_mock): assert_setup_component(6, "sensor") -def setup_api(data, requests_mock): +def setup_api(hass, data, requests_mock): """Set up API with fake data.""" resource = f"http://localhost{google_wifi.ENDPOINT}" now = datetime(1970, month=1, day=1) @@ -84,6 +84,10 @@ def setup_api(data, requests_mock): "units": cond_list[1], "icon": cond_list[2], } + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + sensor.hass = hass + return api, sensor_dict @@ -96,7 +100,7 @@ def fake_delay(hass, ha_delay): def test_name(requests_mock): """Test the name.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] test_name = sensor_dict[name]["name"] @@ -105,7 +109,7 @@ def test_name(requests_mock): def test_unit_of_measurement(requests_mock): """Test the unit of measurement.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] assert sensor_dict[name]["units"] == sensor.unit_of_measurement @@ -113,7 +117,7 @@ def test_unit_of_measurement(requests_mock): def test_icon(requests_mock): """Test the icon.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] assert sensor_dict[name]["icon"] == sensor.icon @@ -121,7 +125,7 @@ def test_icon(requests_mock): def test_state(hass, requests_mock): """Test the initial state.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -140,7 +144,7 @@ def test_state(hass, requests_mock): def test_update_when_value_is_none(hass, requests_mock): """Test state gets updated to unknown when sensor returns no data.""" - api, sensor_dict = setup_api(None, requests_mock) + api, sensor_dict = setup_api(hass, None, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] fake_delay(hass, 2) @@ -150,7 +154,7 @@ def test_update_when_value_is_none(hass, requests_mock): def test_update_when_value_changed(hass, requests_mock): """Test state gets updated when sensor returns a new status.""" - api, sensor_dict = setup_api(MOCK_DATA_NEXT, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA_NEXT, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -173,7 +177,7 @@ def test_update_when_value_changed(hass, requests_mock): def test_when_api_data_missing(hass, requests_mock): """Test state logs an error when data is missing.""" - api, sensor_dict = setup_api(MOCK_DATA_MISSING, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA_MISSING, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -183,12 +187,12 @@ def test_when_api_data_missing(hass, requests_mock): assert sensor.state == STATE_UNKNOWN -def test_update_when_unavailable(requests_mock): +def test_update_when_unavailable(hass, requests_mock): """Test state updates when Google Wifi unavailable.""" - api, sensor_dict = setup_api(None, requests_mock) + api, sensor_dict = setup_api(hass, None, requests_mock) api.update = Mock( "google_wifi.GoogleWifiAPI.update", - side_effect=update_side_effect(requests_mock), + side_effect=update_side_effect(hass, requests_mock), ) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] @@ -196,8 +200,8 @@ def test_update_when_unavailable(requests_mock): assert sensor.state is None -def update_side_effect(requests_mock): +def update_side_effect(hass, requests_mock): """Mock representation of update function.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) api.data = None api.available = False diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 662448c8118..096052fd6cf 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -149,30 +149,6 @@ async def test_one_plant_on_account(hass): assert result["data"][CONF_PLANT_ID] == "123456" -async def test_import_one_plant(hass): - """Test import step with a single plant.""" - import_data = FIXTURE_USER_INPUT.copy() - - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch( - "growattServer.GrowattApi.plant_list", - return_value=GROWATT_PLANT_LIST_RESPONSE, - ), patch( - "homeassistant.components.growatt_server.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PLANT_ID] == "123456" - - async def test_existing_plant_configured(hass): """Test entering an existing plant_id.""" entry = MockConfigEntry(domain=DOMAIN, unique_id="123456") diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 7909d8f0239..8de44843626 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -911,7 +911,6 @@ async def test_statistics_during_period( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index d12cc8d9a7b..fb4a0f4c1da 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -23,6 +23,7 @@ from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, + SERVICE_SAVE_PERSISTENT_STATES, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -543,3 +544,18 @@ async def test_stop_homeassistant(hass): assert not mock_check.called await hass.async_block_till_done() assert mock_restart.called + + +async def test_save_persistent_states(hass): + """Test we can call save_persistent_states.""" + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.helpers.restore_state.RestoreStateData.async_save_persistent_states", + return_value=None, + ) as mock_save: + await hass.services.async_call( + "homeassistant", + SERVICE_SAVE_PERSISTENT_STATES, + blocking=True, + ) + assert mock_save.called diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py deleted file mode 100644 index 6b1d87e3f54..00000000000 --- a/tests/components/homekit/common.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Collection of fixtures and functions for the HomeKit tests.""" -from unittest.mock import Mock - -EMPTY_8_6_JPEG = b"empty_8_6" - - -def mock_turbo_jpeg( - first_width=None, second_width=None, first_height=None, second_height=None -): - """Mock a TurboJPEG instance.""" - mocked_turbo_jpeg = Mock() - mocked_turbo_jpeg.decode_header.side_effect = [ - (first_width, first_height, 0, 0), - (second_width, second_height, 0, 0), - ] - mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG - return mocked_turbo_jpeg diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 5904d1c11c6..975864b42d5 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_SW_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( 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): diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index b9df572a699..991965b30b5 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -7,6 +7,7 @@ from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components import camera, ffmpeg +from homeassistant.components.camera.img_util import TurboJPEGSingleton from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AUDIO_CODEC_COPY, @@ -26,14 +27,13 @@ from homeassistant.components.homekit.const import ( VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, ) -from homeassistant.components.homekit.img_util import TurboJPEGSingleton from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_switches import Switch from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from .common import mock_turbo_jpeg +from tests.components.camera.common import mock_turbo_jpeg MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index f75e6bf19ac..de0fd532ec9 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,21 +1,23 @@ """Test different accessory types: Lights.""" +from datetime import timedelta + from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest from homeassistant.components.homekit.const import ATTR_VALUE -from homeassistant.components.homekit.type_lights import Light +from homeassistant.components.homekit.type_lights import ( + CHANGE_COALESCE_TIME_WINDOW, + Light, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGB, - COLOR_MODE_XY, DOMAIN, ) from homeassistant.const import ( @@ -29,8 +31,16 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service + + +async def _wait_for_light_coalesce(hass): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=CHANGE_COALESCE_TIME_WINDOW) + ) + await hass.async_block_till_done() async def test_light_basic(hass, hk_driver, events): @@ -44,45 +54,41 @@ async def test_light_basic(hass, hk_driver, events): assert acc.aid == 1 assert acc.category == 5 # Lightbulb - assert acc.char_on_primary.value + assert acc.char_on.value await acc.run() await hass.async_block_till_done() - assert acc.char_on_primary.value == 1 + assert acc.char_on.value == 1 hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} ] }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_on_primary.client_update_value, 1) - await hass.async_block_till_done() + acc.char_on.client_update_value(1) + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 @@ -94,16 +100,12 @@ async def test_light_basic(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 0, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 @@ -128,17 +130,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -147,21 +149,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 @@ -173,21 +171,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 40, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[1] assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 @@ -199,21 +193,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 0, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 @@ -223,24 +213,24 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # in update_state hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 # Ensure floats are handled hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 22 + assert acc.char_brightness.value == 22 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 43 + assert acc.char_brightness.value == 43 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 async def test_light_color_temperature(hass, hk_driver, events): @@ -256,33 +246,30 @@ async def test_light_color_temperature(hass, hk_driver, events): acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 await acc.run() await hass.async_block_till_done() - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, } ] }, "mock_addr", ) - await hass.async_add_executor_job( - acc.char_color_temperature.client_update_value, 250 - ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 @@ -292,11 +279,7 @@ async def test_light_color_temperature(hass, hk_driver, events): @pytest.mark.parametrize( "supported_color_modes", - [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], - ], + [["color_temp", "hs"], ["color_temp", "rgb"], ["color_temp", "xy"]], ) async def test_light_color_temperature_and_rgb_color( hass, hk_driver, events, supported_color_modes @@ -310,93 +293,190 @@ async def test_light_color_temperature_and_rgb_color( { ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_COLOR_TEMP: 190, - ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGB, ATTR_HS_COLOR: (260, 90), }, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) - assert acc.char_hue.value == 260 - assert acc.char_saturation.value == 90 - assert acc.char_on_primary.value == 1 - assert acc.char_on_secondary.value == 0 - assert acc.char_brightness_primary.value == 100 - assert acc.char_brightness_secondary.value == 100 - - assert hasattr(acc, "char_color_temperature") - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 224, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - ATTR_BRIGHTNESS: 127, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 - assert acc.char_brightness_primary.value == 50 - assert acc.char_brightness_secondary.value == 50 - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 352, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 352 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 hk_driver.add_accessory(acc) + assert acc.char_color_temp.value == 190 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 16 + + assert hasattr(acc, "char_color_temp") + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 224 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 27 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 352 + assert acc.char_hue.value == 28 + assert acc.char_saturation.value == 61 + + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 250, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 50, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 50, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{PERCENTAGE}, color temperature at 250" + ) + + # Only set Hue hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_hue_iid, - HAP_REPR_VALUE: 145, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_saturation_iid, - HAP_REPR_VALUE: 75, - }, + HAP_REPR_VALUE: 30, + } ] }, "mock_addr", ) - assert acc.char_hue.value == 145 - assert acc.char_saturation.value == 75 + await _wait_for_light_coalesce(hass) + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_HS_COLOR] == (30, 50) + assert events[-1].data[ATTR_VALUE] == "set color at (30, 50)" + + # Only set Saturation hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, - HAP_REPR_VALUE: 200, - }, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 20, + } ] }, "mock_addr", ) - assert acc.char_color_temperature.value == 200 + await _wait_for_light_coalesce(hass) + assert call_turn_on[2] + assert call_turn_on[2].data[ATTR_HS_COLOR] == (30, 20) + + assert events[-1].data[ATTR_VALUE] == "set color at (30, 20)" + + # Generate a conflict by setting hue and then color temp + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 80, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 320, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[3] + assert call_turn_on[3].data[ATTR_COLOR_TEMP] == 320 + assert events[-1].data[ATTR_VALUE] == "color temperature at 320" + + # Generate a conflict by setting color temp then saturation + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 404, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 35, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[4] + assert call_turn_on[4].data[ATTR_HS_COLOR] == (80, 35) + assert events[-1].data[ATTR_VALUE] == "set color at (80, 35)" + + # Set from HASS + hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (100, 100)}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 404 + assert acc.char_hue.value == 100 + assert acc.char_saturation.value == 100 @pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) @@ -444,7 +524,7 @@ async def test_light_rgb_color(hass, hk_driver, events, supported_color_modes): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) @@ -476,13 +556,13 @@ async def test_light_restore(hass, hk_driver, events): hk_driver.add_accessory(acc) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == [] - assert acc.char_on_primary.value == 0 + assert acc.chars == [] + assert acc.char_on.value == 0 acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == ["Brightness"] - assert acc.char_on_primary.value == 0 + assert acc.chars == ["Brightness"] + assert acc.char_on.value == 0 async def test_light_set_brightness_and_color(hass, hk_driver, events): @@ -503,19 +583,19 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)}) await hass.async_block_till_done() @@ -528,14 +608,10 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { @@ -552,7 +628,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 @@ -565,6 +641,26 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): ) +async def test_light_min_max_mireds(hass, hk_driver, events): + """Test mireds are forced to ints.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], + ATTR_BRIGHTNESS: 255, + ATTR_MAX_MIREDS: 500.5, + ATTR_MIN_MIREDS: 100.5, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + acc.char_color_temp.properties["maxValue"] == 500 + acc.char_color_temp.properties["minValue"] == 100 + + async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): """Test light with all chars in one go.""" entity_id = "light.demo" @@ -583,22 +679,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 + assert acc.char_color_temp.value == 224 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -606,26 +702,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 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 diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py new file mode 100644 index 00000000000..86fb9f65f11 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -0,0 +1,84 @@ +"""Make sure that an Arlo Baby can be setup.""" + +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_arlo_baby_setup(hass): + """Test that an Arlo Baby can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "arlo_baby.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + sensors = [ + ( + "camera.arlobabya0", + "homekit-00A0000000000-aid:1", + "ArloBabyA0", + ), + ( + "binary_sensor.arlobabya0", + "homekit-00A0000000000-500", + "ArloBabyA0", + ), + ( + "sensor.arlobabya0_battery", + "homekit-00A0000000000-700", + "ArloBabyA0 Battery", + ), + ( + "sensor.arlobabya0_humidity", + "homekit-00A0000000000-900", + "ArloBabyA0 Humidity", + ), + ( + "sensor.arlobabya0_temperature", + "homekit-00A0000000000-1000", + "ArloBabyA0 Temperature", + ), + ( + "sensor.arlobabya0_air_quality", + "homekit-00A0000000000-aid:1-sid:800-cid:802", + "ArloBabyA0 - Air Quality", + ), + ( + "light.arlobabya0", + "homekit-00A0000000000-1100", + "ArloBabyA0", + ), + ] + + device_ids = set() + + for (entity_id, unique_id, friendly_name) in sensors: + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id + + helper = Helper( + hass, + entity_id, + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == friendly_name + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Netgear, Inc" + assert device.name == "ArloBabyA0" + assert device.model == "ABC1000" + assert device.sw_version == "1.10.931" + assert device.via_device_id is None + + device_ids.add(entry.device_id) + + # All entities should be part of same device + assert len(device_ids) == 1 diff --git a/tests/components/homekit_controller/test_air_quality.py b/tests/components/homekit_controller/test_air_quality.py index 52c79f2b28a..f75335ca357 100644 --- a/tests/components/homekit_controller/test_air_quality.py +++ b/tests/components/homekit_controller/test_air_quality.py @@ -2,6 +2,8 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.helpers import entity_registry as er + from tests.components.homekit_controller.common import setup_test_component @@ -35,6 +37,12 @@ async def test_air_quality_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit temperature sensor accessory.""" helper = await setup_test_component(hass, create_air_quality_sensor_service) + entity_registry = er.async_get(hass) + entity_registry.async_update_entity( + entity_id="air_quality.testdevice", disabled_by=None + ) + await hass.async_block_till_done() + state = await helper.poll_and_get_state() assert state.state == "4444" 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 diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index dd6bf980d0f..e8aaf906936 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -39,8 +39,7 @@ async def test_state(hass) -> None: 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 state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING assert "device_class" not in state.attributes future_now = dt_util.utcnow() + timedelta(seconds=3600) @@ -58,8 +57,7 @@ async def test_state(hass) -> None: 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() + assert state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING async def test_restore_state(hass: HomeAssistant) -> None: @@ -71,8 +69,8 @@ async def test_restore_state(hass: HomeAssistant) -> None: "sensor.integration", "100.0", { - "last_reset": "2019-10-06T21:00:00", "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, }, ), ), @@ -96,7 +94,6 @@ async def test_restore_state(hass: HomeAssistant) -> None: 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: @@ -107,9 +104,7 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None: State( "sensor.integration", "INVALID", - { - "last_reset": "2019-10-06T21:00:00.000000", - }, + {}, ), ), ) @@ -130,8 +125,7 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None: 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 state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING assert "device_class" not in state.attributes diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py new file mode 100644 index 00000000000..48b871b85e4 --- /dev/null +++ b/tests/components/knx/test_binary_sensor.py @@ -0,0 +1,205 @@ +"""Test KNX binary sensor.""" +from datetime import timedelta + +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE +from homeassistant.components.knx.schema import BinarySensorSchema +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .conftest import KNXTestKit + +from tests.common import async_capture_events, async_fire_time_changed + + +async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary sensor and inverted binary_sensor.""" + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + }, + { + CONF_NAME: "test_invert", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_INVERT: True, + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + + # StateUpdater initialize state + await knx.assert_read("1/1/1") + await knx.assert_read("2/2/2") + await knx.receive_response("1/1/1", True) + await knx.receive_response("2/2/2", False) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_ON + assert state_invert.state is STATE_ON + + # receive OFF telegram + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", True) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_OFF + assert state_invert.state is STATE_OFF + + # receive ON telegram + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", False) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_ON + assert state_invert.state is STATE_ON + + # binary_sensor does not respond to read + await knx.receive_read("1/1/1") + await knx.receive_read("2/2/2") + await knx.assert_telegram_count(0) + + +async def test_binary_sensor_ignore_internal_state( + hass: HomeAssistant, knx: KNXTestKit +): + """Test KNX binary_sensor with ignore_internal_state.""" + events = async_capture_events(hass, "state_changed") + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + CONF_SYNC_STATE: False, + }, + { + CONF_NAME: "test_ignore", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE: True, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + # binary_sensor defaults to STATE_OFF - state change form None + assert len(events) == 2 + + # receive initial ON telegram + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + assert len(events) == 4 + + # receive second ON telegram - ignore_internal_state shall force state_changed event + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + assert len(events) == 5 + + # receive first OFF telegram + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", False) + await hass.async_block_till_done() + assert len(events) == 7 + + # receive second OFF telegram - ignore_internal_state shall force state_changed event + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", False) + await hass.async_block_till_done() + assert len(events) == 8 + + +async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary_sensor with context timeout.""" + async_fire_time_changed(hass, dt.utcnow()) + events = async_capture_events(hass, "state_changed") + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_CONTEXT_TIMEOUT: 1, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + assert len(events) == 1 + events.pop() + + # receive initial ON telegram + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + # no change yet - still in 1 sec context (additional async_block_till_done needed for time change) + assert len(events) == 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF + assert state.attributes.get("counter") == 0 + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + # additional async_block_till_done needed event capture + await hass.async_block_till_done() + assert len(events) == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 1 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + + # receive 2 telegrams in context + await knx.receive_write("2/2/2", True) + await knx.receive_write("2/2/2", True) + assert len(events) == 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + await hass.async_block_till_done() + assert len(events) == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + + +async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary_sensor with reset_after function.""" + async_fire_time_changed(hass, dt.utcnow()) + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_RESET_AFTER: 1, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + + # receive ON telegram + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + # state reset after after timeout + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py new file mode 100644 index 00000000000..16ea5e8d385 --- /dev/null +++ b/tests/components/knx/test_sensor.py @@ -0,0 +1,95 @@ +"""Test KNX sensor.""" +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE +from homeassistant.components.knx.schema import SensorSchema +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +from tests.common import async_capture_events + + +async def test_sensor(hass: HomeAssistant, knx: KNXTestKit): + """Test simple KNX sensor.""" + + await knx.setup_integration( + { + SensorSchema.PLATFORM_NAME: { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "1/1/1", + CONF_TYPE: "current", # 2 byte unsigned int + } + } + ) + assert len(hass.states.async_all()) == 1 + state = hass.states.get("sensor.test") + assert state.state is STATE_UNKNOWN + + # StateUpdater initialize state + await knx.assert_read("1/1/1") + await knx.receive_response("1/1/1", (0, 40)) + state = hass.states.get("sensor.test") + assert state.state == "40" + + # update from KNX + await knx.receive_write("1/1/1", (0x03, 0xE8)) + state = hass.states.get("sensor.test") + assert state.state == "1000" + + # don't answer to GroupValueRead requests + await knx.receive_read("1/1/1") + await knx.assert_no_telegram() + + +async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX sensor with always_callback.""" + + events = async_capture_events(hass, "state_changed") + await knx.setup_integration( + { + SensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + CONF_SYNC_STATE: False, + CONF_TYPE: "percentU8", + }, + { + CONF_NAME: "test_always", + CONF_STATE_ADDRESS: "2/2/2", + SensorSchema.CONF_ALWAYS_CALLBACK: True, + CONF_SYNC_STATE: False, + CONF_TYPE: "percentU8", + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + # state changes form None to "unknown" + assert len(events) == 2 + + # receive initial telegram + await knx.receive_write("1/1/1", (0x42,)) + await knx.receive_write("2/2/2", (0x42,)) + await hass.async_block_till_done() + assert len(events) == 4 + + # receive second telegram with identical payload + # always_callback shall force state_changed event + await knx.receive_write("1/1/1", (0x42,)) + await knx.receive_write("2/2/2", (0x42,)) + await hass.async_block_till_done() + assert len(events) == 5 + + # receive telegram with different payload + await knx.receive_write("1/1/1", (0xFA,)) + await knx.receive_write("2/2/2", (0xFA,)) + await hass.async_block_till_done() + assert len(events) == 7 + + # receive telegram with second payload again + # always_callback shall force state_changed event + await knx.receive_write("1/1/1", (0xFA,)) + await knx.receive_write("2/2/2", (0xFA,)) + await hass.async_block_till_done() + assert len(events) == 8 diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index a5f5b955882..dbc8c39790c 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -26,6 +26,7 @@ async def test_sleep_time_sensor_with_none_state(hass): sensor = LitterRobotSleepTimeSensor( robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time" ) + sensor.hass = hass assert sensor assert sensor.state is None diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 1f5cc5fd04f..4032e29b743 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -121,7 +121,9 @@ def port_fixture(): @pytest.fixture(name="sensor") def sensor_fixture(hass, port): """Sensor fixture.""" - return mfi.MfiSensor(port, hass) + sensor = mfi.MfiSensor(port, hass) + sensor.hass = hass + return sensor async def test_name(port, sensor): diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index e827b5dfbd2..26e9441f9fc 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -83,10 +83,11 @@ async def aiohttp_client_update_good_read(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_co2_sensor(mock_function): +async def test_co2_sensor(mock_function, hass): """Test CO2 sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor(client, mhz19.SENSOR_CO2, None, "name") + sensor.hass = hass sensor.update() assert sensor.name == "name: CO2" @@ -97,10 +98,11 @@ async def test_co2_sensor(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor(mock_function): +async def test_temperature_sensor(mock_function, hass): """Test temperature sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor(client, mhz19.SENSOR_TEMPERATURE, None, "name") + sensor.hass = hass sensor.update() assert sensor.name == "name: Temperature" @@ -111,12 +113,13 @@ async def test_temperature_sensor(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor_f(mock_function): +async def test_temperature_sensor_f(mock_function, hass): """Test temperature sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor( client, mhz19.SENSOR_TEMPERATURE, TEMP_FAHRENHEIT, "name" ) + sensor.hass = hass sensor.update() assert sensor.state == 75.2 diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index db960f448ff..35688d2f608 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +from dataclasses import dataclass from datetime import timedelta import logging from unittest import mock @@ -6,7 +7,11 @@ from unittest import mock from pymodbus.exceptions import ModbusException import pytest -from homeassistant.components.modbus.const import DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN +from homeassistant.components.modbus.const import ( + DEFAULT_HUB, + MODBUS_DOMAIN as DOMAIN, + TCP, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -21,9 +26,24 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, mock_restore_cache TEST_MODBUS_NAME = "modbusTest" +TEST_ENTITY_NAME = "test_entity" +TEST_MODBUS_HOST = "modbusHost" +TEST_PORT_TCP = 5501 +TEST_PORT_SERIAL = "usb01" + _LOGGER = logging.getLogger(__name__) +@dataclass +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + self.bits = register_words + + @pytest.fixture def mock_pymodbus(): """Mock pymodbus.""" @@ -39,24 +59,39 @@ def mock_pymodbus(): yield mock_pb -@pytest.fixture -async def mock_modbus(hass, do_config): +@pytest.fixture( + params=[ + {"testLoad": True}, + ], +) +async def mock_modbus(hass, caplog, request, do_config): """Load integration modbus using mocked pymodbus.""" + + caplog.set_level(logging.WARNING) config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, **do_config, } ] } + mock_pb = mock.MagicMock() with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True - ) as mock_pb: - assert await async_setup_component(hass, DOMAIN, config) is True + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ): + mock_pb.read_coils.return_value = ReadResult([0x00]) + read_result = ReadResult([0x00, 0x00]) + mock_pb.read_discrete_inputs.return_value = read_result + mock_pb.read_input_registers.return_value = read_result + mock_pb.read_holding_registers.return_value = read_result + if request.param["testLoad"]: + assert await async_setup_component(hass, DOMAIN, config) is True + else: + await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield mock_pb @@ -68,14 +103,11 @@ async def mock_test_state(hass, request): return request.param -# dataclass -class ReadResult: - """Storage class for register read results.""" - - def __init__(self, register_words): - """Init.""" - self.registers = register_words - self.bits = register_words +@pytest.fixture +async def mock_ha(hass): + """Load homeassistant to allow service calls.""" + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() async def base_test( @@ -88,7 +120,6 @@ async def base_test( register_words, expected, method_discovery=False, - check_config_only=False, config_modbus=None, scan_interval=None, expect_init_to_fail=False, @@ -100,9 +131,9 @@ async def base_test( config_modbus = { DOMAIN: { CONF_NAME: DEFAULT_HUB, - CONF_TYPE: "tcp", - CONF_HOST: "modbusTest", - CONF_PORT: 5001, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, } @@ -173,8 +204,6 @@ async def base_test( assert device is None elif device is None: pytest.fail("CONFIG failed, see output") - if check_config_only: - return # Trigger update call with time_changed event now = now + timedelta(seconds=scan_interval + 60) @@ -185,52 +214,3 @@ async def base_test( # Check state entity_id = f"{entity_domain}.{device_name}" return hass.states.get(entity_id).state - - -async def base_config_test( - hass, - config_device, - device_name, - entity_domain, - array_name_discovery, - array_name_old_config, - method_discovery=False, - config_modbus=None, - expect_init_to_fail=False, - expect_setup_to_fail=False, -): - """Check config of device for given config.""" - - await base_test( - hass, - config_device, - device_name, - entity_domain, - array_name_discovery, - array_name_old_config, - None, - None, - method_discovery=method_discovery, - check_config_only=True, - config_modbus=config_modbus, - expect_init_to_fail=expect_init_to_fail, - expect_setup_to_fail=expect_setup_to_fail, - ) - - -async def prepare_service_update(hass, config): - """Run test for service write_coil.""" - - config_modbus = { - DOMAIN: { - CONF_NAME: DEFAULT_HUB, - CONF_TYPE: "tcp", - CONF_HOST: "modbusTest", - CONF_PORT: 5001, - **config, - }, - } - assert await async_setup_component(hass, DOMAIN, config_modbus) - await hass.async_block_till_done() - assert await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e77fd380a22..fb52ea11090 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -20,10 +20,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -SENSOR_NAME = "test_binary_sensor" -ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -32,7 +31,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, } ] @@ -40,7 +39,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, @@ -89,8 +88,8 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): """Run test for given config.""" state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_BINARY_SENSORS, None, @@ -102,33 +101,34 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): assert state == expected -async def test_service_binary_sensor_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + }, + ], +) +async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - config = { - CONF_BINARY_SENSORS: [ - { - CONF_NAME: SENSOR_NAME, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - } - ] - } - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON @@ -143,7 +143,7 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, } diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 97d2c32ba69..16ef18a60ac 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -22,10 +22,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -CLIMATE_NAME = "test_climate" -ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" +ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -34,7 +33,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -44,7 +43,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -71,18 +70,17 @@ async def test_config_climate(hass, mock_modbus): ) async def test_temperature_climate(hass, regs, expected): """Run test for given config.""" - CLIMATE_NAME = "modbus_test_climate" return state = await base_test( hass, { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_COUNT: 2, }, - CLIMATE_NAME, + TEST_ENTITY_NAME, CLIMATE_DOMAIN, CONF_CLIMATES, None, @@ -94,25 +92,24 @@ async def test_temperature_climate(hass, regs, expected): assert state == expected -async def test_service_climate_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + } + ] + }, + ], +) +async def test_service_climate_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_CLIMATES: [ - { - CONF_NAME: CLIMATE_NAME, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - } - ] - } - mock_pymodbus.read_input_registers.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -120,34 +117,75 @@ async def test_service_climate_update(hass, mock_pymodbus): @pytest.mark.parametrize( - "data_type, temperature, result", + "temperature, result, do_config", [ - (DATA_TYPE_INT16, 35, [0x00]), - (DATA_TYPE_INT32, 36, [0x00, 0x00]), - (DATA_TYPE_FLOAT32, 37.5, [0x00, 0x00]), - (DATA_TYPE_FLOAT64, "39", [0x00, 0x00, 0x00, 0x00]), + ( + 35, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_INT16, + } + ] + }, + ), + ( + 36, + [0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_INT32, + } + ] + }, + ), + ( + 37.5, + [0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_FLOAT32, + } + ] + }, + ), + ( + "39", + [0x00, 0x00, 0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_FLOAT64, + } + ] + }, + ), ], ) async def test_service_climate_set_temperature( - hass, data_type, temperature, result, mock_pymodbus + hass, temperature, result, mock_modbus, mock_ha ): - """Run test for service homeassistant.update_entity.""" - config = { - CONF_CLIMATES: [ - { - CONF_NAME: CLIMATE_NAME, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_DATA_TYPE: data_type, - } - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult(result) - await prepare_service_update( - hass, - config, - ) + """Test set_temperature.""" + mock_modbus.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_temperature", @@ -174,7 +212,7 @@ test_value.attributes = {ATTR_TEMPERATURE: 37} { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SCAN_INTERVAL: 0, diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 8d7e7e39cf8..a315d8176ae 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -29,10 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -COVER_NAME = "test_cover" -ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" +ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -41,7 +40,7 @@ ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -50,7 +49,7 @@ ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SLAVE: 10, @@ -95,12 +94,12 @@ async def test_coil_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - COVER_NAME, + TEST_ENTITY_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -142,11 +141,11 @@ async def test_register_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - COVER_NAME, + TEST_ENTITY_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -158,28 +157,27 @@ async def test_register_cover(hass, regs, expected): assert state == expected -async def test_service_cover_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + }, + ], +) +async def test_service_cover_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_COVERS: [ - { - CONF_NAME: COVER_NAME, - CONF_ADDRESS: 1234, - CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - } - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -202,7 +200,7 @@ async def test_service_cover_update(hass, mock_pymodbus): { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_STATE_OPEN: 1, @@ -223,51 +221,52 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == test_state -async def test_service_cover_move(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, + }, + { + CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1235, + CONF_SCAN_INTERVAL: 0, + }, + ] + }, + ], +) +async def test_service_cover_move(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" ENTITY_ID2 = f"{ENTITY_ID}2" - config = { - CONF_COVERS: [ - { - CONF_NAME: COVER_NAME, - CONF_ADDRESS: 1234, - CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_SCAN_INTERVAL: 0, - }, - { - CONF_NAME: f"{COVER_NAME}2", - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_ADDRESS: 1234, - CONF_SCAN_INTERVAL: 0, - }, - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OPEN - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_pymodbus.reset() - mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") + mock_modbus.reset() + mock_modbus.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert mock_pymodbus.read_holding_registers.called + assert mock_modbus.read_holding_registers.called assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") + mock_modbus.read_coils.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 13714d6bd0e..821a5cace99 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -15,6 +15,7 @@ from homeassistant.components.modbus.const import ( CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.const import ( CONF_ADDRESS, @@ -33,10 +34,15 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) -FAN_NAME = "test_fan" -ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" +ENTITY_ID = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -45,7 +51,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -53,7 +59,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -62,7 +68,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -79,7 +85,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -96,7 +102,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -113,7 +119,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -165,13 +171,13 @@ async def test_all_fan(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - FAN_NAME, + TEST_ENTITY_NAME, FAN_DOMAIN, CONF_FANS, None, @@ -194,7 +200,7 @@ async def test_all_fan(hass, call_type, regs, verify, expected): { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -210,22 +216,22 @@ async def test_restore_state_fan(hass, mock_test_state, mock_modbus): async def test_fan_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{FAN_DOMAIN}.{FAN_NAME}2" + ENTITY_ID2 = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{FAN_NAME}2", - CONF_ADDRESS: 17, + CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, @@ -277,30 +283,29 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_fan_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_FANS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_fan_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_FANS: [ - { - CONF_NAME: FAN_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8b8d063bf02..3eb1beb460f 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, @@ -50,8 +51,12 @@ from homeassistant.components.modbus.const import ( DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, + RTUOVERTCP, + SERIAL, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, + TCP, + UDP, ) from homeassistant.components.modbus.validators import ( number_validator, @@ -78,15 +83,17 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_MODBUS_NAME, + TEST_PORT_SERIAL, + TEST_PORT_TCP, + ReadResult, +) from tests.common import async_fire_time_changed -TEST_SENSOR_NAME = "testSensor" -TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" -TEST_HOST = "modbusTestHost" -TEST_MODBUS_NAME = "modbusTest" - @pytest.fixture async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus): @@ -127,17 +134,17 @@ async def test_number_validator(): "do_config", [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_STRING, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_INT, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_INT, CONF_SWAP: CONF_SWAP_BYTE, @@ -156,29 +163,29 @@ async def test_ok_struct_validator(do_config): "do_config", [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_INT, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: "no good", }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 20, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: ">f", }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 1, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: ">f", @@ -199,59 +206,60 @@ async def test_exception_struct_validator(do_config): "do_config", [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "udp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: UDP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "udp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: UDP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "rtuovertcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: RTUOVERTCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "rtuovertcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: RTUOVERTCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "serial", + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, + CONF_MSG_WAIT: 100, }, { - CONF_TYPE: "serial", + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, CONF_NAME: TEST_MODBUS_NAME, @@ -259,43 +267,43 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_DELAY: 5, }, [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME + "2", + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_NAME: f"{TEST_MODBUS_NAME}2", }, { - CONF_TYPE: "serial", + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, - CONF_NAME: TEST_MODBUS_NAME + "3", + CONF_NAME: f"{TEST_MODBUS_NAME}3", }, ], { # Special test for scan_interval validator with scan_interval: 0 - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SENSORS: [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, CONF_SCAN_INTERVAL: 0, } @@ -318,11 +326,11 @@ SERVICE = "service" [ { CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: "serial", + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, }, @@ -423,14 +431,14 @@ async def mock_modbus_read_pymodbus( config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, do_group: [ { CONF_INPUT_TYPE: do_type, - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: do_scan_interval, } @@ -480,7 +488,7 @@ async def test_pb_read( """Run test for different read.""" # Check state - entity_id = f"{do_domain}.{TEST_SENSOR_NAME}" + entity_id = f"{do_domain}.{TEST_ENTITY_NAME}" state = hass.states.get(entity_id).state assert hass.states.get(entity_id).state @@ -497,9 +505,9 @@ async def test_pymodbus_constructor_fail(hass, caplog): config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, } ] } @@ -520,9 +528,9 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, } ] } @@ -541,19 +549,19 @@ async def test_delay(hass, mock_pymodbus): # We "hijiack" a binary_sensor to make a proper blackbox test. test_delay = 15 test_scan_interval = 5 - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_DELAY: test_delay, CONF_BINARY_SENSORS: [ { CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_NAME: f"{TEST_SENSOR_NAME}", + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 52, CONF_SCAN_INTERVAL: test_scan_interval, }, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index c7b9b820934..486dfdc64f8 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -14,6 +14,7 @@ from homeassistant.components.modbus.const import ( CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.const import ( CONF_ADDRESS, @@ -33,10 +34,15 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) -LIGHT_NAME = "test_light" -ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" +ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -45,7 +51,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -53,7 +59,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -62,7 +68,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -79,7 +85,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -96,7 +102,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -113,7 +119,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -165,13 +171,13 @@ async def test_all_light(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - LIGHT_NAME, + TEST_ENTITY_NAME, LIGHT_DOMAIN, CONF_LIGHTS, None, @@ -194,7 +200,7 @@ async def test_all_light(hass, call_type, regs, verify, expected): { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -213,19 +219,19 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): ENTITY_ID2 = f"{ENTITY_ID}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{LIGHT_NAME}2", - CONF_ADDRESS: 17, + CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, @@ -277,30 +283,29 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_light_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_light_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_LIGHTS: [ - { - CONF_NAME: LIGHT_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index f01a3ef9da5..e69a6be41a4 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the Modbus sensor component.""" -import logging - import pytest from homeassistant.components.modbus.const import ( @@ -37,10 +35,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -SENSOR_NAME = "test_sensor" -ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -49,7 +46,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, } ] @@ -57,7 +54,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -73,7 +70,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -89,7 +86,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_NONE, @@ -99,7 +96,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_BYTE, @@ -109,7 +106,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD, @@ -119,7 +116,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD_BYTE, @@ -133,95 +130,106 @@ async def test_config_sensor(hass, mock_modbus): assert SENSOR_DOMAIN in hass.config.components +@pytest.mark.parametrize("mock_modbus", [{"testLoad": False}], indirect=True) @pytest.mark.parametrize( "do_config,error_message", [ ( { - CONF_ADDRESS: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">no struct", + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">no struct", + }, + ] }, "bad char in struct format", ), ( { - CONF_ADDRESS: 1234, - CONF_COUNT: 2, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">4f", + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_COUNT: 2, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + ] }, "Structure request 16 bytes, but 2 registers have a size of 4 bytes", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "invalid", + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "invalid", + }, + ] }, "bad char in struct format", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "", + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "", + }, + ] }, - "Error in sensor test_sensor. The `structure` field can not be empty", + f"Error in sensor {TEST_ENTITY_NAME}. The `structure` field can not be empty", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "1s", + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "1s", + }, + ] }, "Structure request 1 bytes, but 4 registers have a size of 8 bytes", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 1, - CONF_STRUCTURE: "2s", - CONF_SWAP: CONF_SWAP_WORD, + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 1, + CONF_STRUCTURE: "2s", + CONF_SWAP: CONF_SWAP_WORD, + }, + ] }, - "Error in sensor test_sensor swap(word) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {TEST_ENTITY_NAME} swap(word) not possible due to the registers count: 1, needed: 2", ), ], ) -async def test_config_wrong_struct_sensor( - hass, caplog, do_config, error_message, mock_pymodbus -): +async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, caplog): """Run test for sensor with wrong struct.""" - - config_sensor = { - CONF_NAME: SENSOR_NAME, - **do_config, - } - caplog.set_level(logging.WARNING) - caplog.clear() - - await base_config_test( - hass, - config_sensor, - SENSOR_NAME, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - expect_setup_to_fail=True, - ) - - assert caplog.text.count(error_message) + messages = str([x.message for x in caplog.get_records("setup")]) + assert error_message in messages @pytest.mark.parametrize( @@ -499,8 +507,8 @@ async def test_all_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, **cfg}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_SENSORS, CONF_REGISTERS, @@ -553,8 +561,8 @@ async def test_struct_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, **cfg}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_SENSORS, None, @@ -577,7 +585,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, } @@ -590,27 +598,28 @@ async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state -async def test_service_sensor_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + }, + ], +) +async def test_service_sensor_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - config = { - CONF_SENSORS: [ - { - CONF_NAME: SENSOR_NAME, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - } - ] - } - mock_pymodbus.read_input_registers.return_value = ReadResult([27]) - await prepare_service_update( - hass, - config, - ) + mock_modbus.read_input_registers.return_value = ReadResult([27]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "27" - mock_pymodbus.read_input_registers.return_value = ReadResult([32]) + mock_modbus.read_input_registers.return_value = ReadResult([32]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index c620429aad2..fb929d26caf 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -16,6 +16,7 @@ from homeassistant.components.modbus.const import ( CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -39,12 +40,17 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) from tests.common import async_fire_time_changed -SWITCH_NAME = "test_switch" -ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" +ENTITY_ID = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -53,7 +59,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -61,7 +67,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -70,7 +76,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -88,7 +94,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -107,7 +113,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -125,7 +131,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -179,13 +185,13 @@ async def test_all_switch(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - SWITCH_NAME, + TEST_ENTITY_NAME, SWITCH_DOMAIN, CONF_SWITCHES, None, @@ -208,7 +214,7 @@ async def test_all_switch(hass, call_type, regs, verify, expected): { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -224,22 +230,22 @@ async def test_restore_state_switch(hass, mock_test_state, mock_modbus): async def test_switch_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{SWITCH_DOMAIN}.{SWITCH_NAME}2" + ENTITY_ID2 = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{SWITCH_NAME}2", - CONF_ADDRESS: 17, + CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, @@ -291,33 +297,32 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_switch_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SWITCHES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_switch_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_SWITCHES: [ - { - CONF_NAME: SWITCH_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON async def test_delay_switch(hass, mock_pymodbus): @@ -325,12 +330,12 @@ async def test_delay_switch(hass, mock_pymodbus): config = { MODBUS_DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: { 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": {}}) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 15ca9870077..724dec1c93f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -306,6 +306,28 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" +async def test_last_reset_deprecated(hass, mqtt_mock, caplog): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } + }, + ) + await hass.async_block_till_done() + + assert "The 'last_reset_topic' option is deprecated" in caplog.text + assert "The 'last_reset_value_template' option is deprecated" in caplog.text + + async def test_force_update_disabled(hass, mqtt_mock): """Test force update option.""" assert await async_setup_component( diff --git a/tests/components/myq/test_light.py b/tests/components/myq/test_light.py new file mode 100644 index 00000000000..c7b3dbc8427 --- /dev/null +++ b/tests/components/myq/test_light.py @@ -0,0 +1,36 @@ +"""The scene tests for the myq platform.""" + +from homeassistant.const import STATE_OFF, STATE_ON + +from .util import async_init_integration + + +async def test_create_lights(hass): + """Test creation of lights.""" + + await async_init_integration(hass) + + state = hass.states.get("light.garage_door_light_off") + assert state.state == STATE_OFF + expected_attributes = { + "friendly_name": "Garage Door Light Off", + "supported_features": 0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("light.garage_door_light_on") + assert state.state == STATE_ON + expected_attributes = { + "friendly_name": "Garage Door Light On", + "supported_features": 0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 49c32301442..1843e495801 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Generator import json -from typing import Any +from typing import Any, Callable from unittest.mock import MagicMock, patch from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( @@ -27,14 +28,14 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(autouse=True) -def device_tracker_storage(mock_device_tracker_conf): +def device_tracker_storage(mock_device_tracker_conf: list[Device]) -> list[Device]: """Mock out device tracker known devices storage.""" devices = mock_device_tracker_conf return devices @pytest.fixture(name="mqtt") -def mock_mqtt_fixture(hass) -> None: +def mock_mqtt_fixture(hass: HomeAssistant) -> None: """Mock the MQTT integration.""" hass.config.components.add(MQTT_DOMAIN) @@ -75,14 +76,14 @@ def mock_gateway_features( ) -> None: """Mock the gateway features.""" - async def mock_start_persistence(): + async def mock_start_persistence() -> None: """Load nodes from via persistence.""" gateway = transport_class.call_args[0][0] gateway.sensors.update(nodes) tasks.start_persistence.side_effect = mock_start_persistence - async def mock_start(): + async def mock_start() -> None: """Mock the start method.""" gateway = transport_class.call_args[0][0] gateway.on_conn_made(gateway) @@ -97,7 +98,7 @@ def transport_fixture(serial_transport: MagicMock) -> MagicMock: @pytest.fixture(name="serial_entry") -async def serial_entry_fixture(hass) -> MockConfigEntry: +async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" entry = MockConfigEntry( domain=DOMAIN, @@ -120,15 +121,25 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: @pytest.fixture async def integration( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[tuple[MockConfigEntry, Callable[[str], None]], None]: """Set up the mysensors integration with a config entry.""" device = config_entry.data[CONF_DEVICE] config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} config_entry.add_to_hass(hass) + + def receive_message(message_string: str) -> None: + """Receive a message with the transport. + + The message_string parameter is a string in the MySensors message format. + """ + gateway = transport.call_args[0][0] + # node_id;child_id;command;ack;type;payload\n + gateway.logic(message_string) + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - yield config_entry + yield config_entry, receive_message def load_nodes_state(fixture_path: str) -> dict: @@ -151,7 +162,7 @@ def gps_sensor_state_fixture() -> dict: @pytest.fixture -def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor: +def gps_sensor(gateway_nodes: dict[int, Sensor], gps_sensor_state: dict) -> Sensor: """Load the gps sensor.""" nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state) node = nodes[1] @@ -165,8 +176,70 @@ def power_sensor_state_fixture() -> dict: @pytest.fixture -def power_sensor(gateway_nodes, power_sensor_state) -> Sensor: +def power_sensor(gateway_nodes: dict[int, Sensor], power_sensor_state: dict) -> Sensor: """Load the power sensor.""" nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="energy_sensor_state", scope="session") +def energy_sensor_state_fixture() -> dict: + """Load the energy sensor state.""" + return load_nodes_state("mysensors/energy_sensor_state.json") + + +@pytest.fixture +def energy_sensor( + gateway_nodes: dict[int, Sensor], energy_sensor_state: dict +) -> Sensor: + """Load the energy sensor.""" + nodes = update_gateway_nodes(gateway_nodes, energy_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="sound_sensor_state", scope="session") +def sound_sensor_state_fixture() -> dict: + """Load the sound sensor state.""" + return load_nodes_state("mysensors/sound_sensor_state.json") + + +@pytest.fixture +def sound_sensor(gateway_nodes: dict[int, Sensor], sound_sensor_state: dict) -> Sensor: + """Load the sound sensor.""" + nodes = update_gateway_nodes(gateway_nodes, sound_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="distance_sensor_state", scope="session") +def distance_sensor_state_fixture() -> dict: + """Load the distance sensor state.""" + return load_nodes_state("mysensors/distance_sensor_state.json") + + +@pytest.fixture +def distance_sensor( + gateway_nodes: dict[int, Sensor], distance_sensor_state: dict +) -> Sensor: + """Load the distance sensor.""" + nodes = update_gateway_nodes(gateway_nodes, distance_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="temperature_sensor_state", scope="session") +def temperature_sensor_state_fixture() -> dict: + """Load the temperature sensor state.""" + return load_nodes_state("mysensors/temperature_sensor_state.json") + + +@pytest.fixture +def temperature_sensor( + gateway_nodes: dict[int, Sensor], temperature_sensor_state: dict +) -> Sensor: + """Load the temperature sensor.""" + nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 6edddc68592..d648aebdefd 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -1,31 +1,158 @@ """Provide tests for mysensors sensor platform.""" +from __future__ import annotations +from typing import Callable -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem + +from tests.common import MockConfigEntry -async def test_gps_sensor(hass, gps_sensor, integration): +async def test_gps_sensor( + hass: HomeAssistant, + gps_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" + _, receive_message = integration state = hass.states.get(entity_id) + assert state assert state.state == "40.741894,-73.989311,12" + altitude = 0 + new_coords = "40.782,-73.965" + message_string = f"1;1;1;0;49;{new_coords},{altitude}\n" -async def test_power_sensor(hass, power_sensor, integration): + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == f"{new_coords},{altitude}" + + +async def test_power_sensor( + hass: HomeAssistant, + power_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a power sensor.""" entity_id = "sensor.power_sensor_1_1" state = hass.states.get(entity_id) + assert state assert state.state == "1200" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + + +async def test_energy_sensor( + hass: HomeAssistant, + energy_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test an energy sensor.""" + entity_id = "sensor.energy_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "18000" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + + +async def test_sound_sensor( + hass: HomeAssistant, + sound_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a sound sensor.""" + entity_id = "sensor.sound_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "10" + assert state.attributes[ATTR_ICON] == "mdi:volume-high" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB" + + +async def test_distance_sensor( + hass: HomeAssistant, + distance_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a distance sensor.""" + entity_id = "sensor.distance_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "15" + assert state.attributes[ATTR_ICON] == "mdi:ruler" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" + + +@pytest.mark.parametrize( + "unit_system, unit", + [(METRIC_SYSTEM, TEMP_CELSIUS), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT)], +) +async def test_temperature_sensor( + hass: HomeAssistant, + temperature_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], + unit_system: UnitSystem, + unit: str, +) -> None: + """Test a temperature sensor.""" + entity_id = "sensor.temperature_sensor_1_1" + hass.config.units = unit_system + _, receive_message = integration + temperature = "22.0" + message_string = f"1;1;1;0;0;{temperature}\n" + + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == temperature + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index c5850ce719d..ce9a221007a 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -19,6 +19,9 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, @@ -212,12 +215,12 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") assert state assert state.state == "19" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sds011_particulate_matter_10" @@ -228,12 +231,12 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state assert state.state == "11" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sds011_particulate_matter_2_5" @@ -244,12 +247,12 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") assert state assert state.state == "31" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM1 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" @@ -260,12 +263,12 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") assert state assert state.state == "21" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") assert entry @@ -274,12 +277,12 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_2_5") assert state assert state.state == "34" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_2_5" @@ -295,7 +298,7 @@ async def test_sensor(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_4_0" diff --git a/tests/components/nest/device_info_test.py b/tests/components/nest/device_info_test.py index a0c6973c1d6..90b70f61d15 100644 --- a/tests/components/nest/device_info_test.py +++ b/tests/components/nest/device_info_test.py @@ -93,11 +93,11 @@ def test_device_invalid_type(): device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" - assert device_info.device_model == "Unknown" + assert device_info.device_model is None assert device_info.device_brand == "Google Nest" assert device_info.device_info == { "identifiers": {("nest", "some-device-id")}, "name": "My Doorbell", "manufacturer": "Google Nest", - "model": "Unknown", + "model": None, } diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index cc18e8cd3ae..dfdfd58d546 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -208,5 +208,5 @@ async def test_device_with_unknown_type(hass): device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "My Sensor" - assert device.model == "Unknown" + assert device.model is None assert device.identifiers == {("nest", "some-device-id")} 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/nmap_tracker/__init__.py b/tests/components/nmap_tracker/__init__.py new file mode 100644 index 00000000000..f5e0c85df31 --- /dev/null +++ b/tests/components/nmap_tracker/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nmap Tracker integration.""" diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py new file mode 100644 index 00000000000..6365dd7407a --- /dev/null +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -0,0 +1,301 @@ +"""Test the Nmap Tracker config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.components.nmap_tracker.const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, +) +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import CoreState, HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] +) +async def test_form(hass: HomeAssistant, hosts: str) -> None: + """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"] == {} + + schema_defaults = result["data_schema"]({}) + assert CONF_SCAN_INTERVAL not in schema_defaults + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == f"Nmap Tracker {hosts}" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_range(hass: HomeAssistant) -> None: + """Test we get the form and can take an ip range.""" + 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"] == {} + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Nmap Tracker 192.168.0.5-12" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_hosts(hass: HomeAssistant) -> None: + """Test invalid hosts passed in.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "not an ip block", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} + + +async def test_form_already_configured(hass: HomeAssistant) -> None: + """Test duplicate host list.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_form_invalid_excludes(hass: HomeAssistant) -> None: + """Test invalid excludes passed in.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "3.3.3.3", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "not an exclude", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test we can edit options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.1.0/24", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + hass.state = CoreState.stopped + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + assert result["data_schema"]({}) == { + CONF_EXCLUDE: "4.4.4.4", + CONF_HOME_INTERVAL: 3, + CONF_HOSTS: "192.168.1.0/24", + CONF_SCAN_INTERVAL: 120, + CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s", + } + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", + CONF_SCAN_INTERVAL: 10, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4,5.5.5.5", + CONF_SCAN_INTERVAL: 10, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "Nmap Tracker 1.2.3.4/20" + assert result["data"] == {} + assert result["options"] == { + CONF_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4,6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_aborts_if_matching(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 32c7da9c78e..33c186e922c 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -39,8 +39,6 @@ async def test_sensors(hass): assert state.state == "0.032" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 10429.5, - "energy_imported_(in_kW)": 4824.2, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Site Now", @@ -52,12 +50,16 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_site_export").state) == 10429.5 + assert float(hass.states.get("sensor.powerwall_site_import").state) == 4824.2 + + export_attributes = hass.states.get("sensor.powerwall_site_export").attributes + assert export_attributes["unit_of_measurement"] == "kWh" + state = hass.states.get("sensor.powerwall_load_now") assert state.state == "1.971" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 1056.8, - "energy_imported_(in_kW)": 4693.0, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Load Now", @@ -69,12 +71,13 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8 + assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0 + state = hass.states.get("sensor.powerwall_battery_now") assert state.state == "-8.55" expected_attributes = { "frequency": 60.0, - "energy_exported_(in_kW)": 3620.0, - "energy_imported_(in_kW)": 4216.2, "instant_average_voltage": 240.6, "unit_of_measurement": "kW", "friendly_name": "Powerwall Battery Now", @@ -86,12 +89,13 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0 + assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2 + state = hass.states.get("sensor.powerwall_solar_now") assert state.state == "10.49" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 9864.2, - "energy_imported_(in_kW)": 28.2, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Solar Now", @@ -103,6 +107,9 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2 + assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2 + state = hass.states.get("sensor.powerwall_charge") assert state.state == "47" expected_attributes = { diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 142d833698d..1e1f24b6eee 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -179,6 +179,20 @@ for i in [1, 2]: assert hass.states.is_state("hello.2", "world") +async def test_using_enumerate(hass): + """Test that enumerate is accepted and executed.""" + source = """ +for index, value in enumerate(["earth", "mars"]): + hass.states.set('hello.{}'.format(index), value) + """ + + hass.async_add_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done() + + assert hass.states.is_state("hello.0", "earth") + assert hass.states.is_state("hello.1", "mars") + + async def test_unpacking_sequence(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 0468cc26a23..83995b0c0ac 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -44,7 +44,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -54,7 +53,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(20.0), "min": approx(20.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -127,7 +125,6 @@ def test_rename_entity(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 5b4b234fbbb..cb54f0404b9 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from sqlalchemy import text +from sqlalchemy.sql.elements import TextClause from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX @@ -253,6 +254,11 @@ def test_end_incomplete_runs(hass_recorder, caplog): def test_perodic_db_cleanups(hass_recorder): """Test perodic db cleanups.""" hass = hass_recorder() - with patch.object(hass.data[DATA_INSTANCE].engine, "execute") as execute_mock: + with patch.object(hass.data[DATA_INSTANCE].engine, "connect") as connect_mock: util.perodic_db_cleanups(hass.data[DATA_INSTANCE]) - assert execute_mock.call_args[0][0] == "PRAGMA wal_checkpoint(TRUNCATE);" + + text_obj = connect_mock.return_value.__enter__.return_value.execute.mock_calls[0][ + 1 + ][0] + assert isinstance(text_obj, TextClause) + assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 1193764da3a..48e741a12a4 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -2,9 +2,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.remote import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -12,6 +9,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index e4edc3b8539..9191851c777 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -1,159 +1,242 @@ """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 renault_api.kamereon import schemas +from renault_api.renault_account import RenaultAccount -from homeassistant.components.renault.const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOCALE, - DOMAIN, +from homeassistant.components.renault.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, ) -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 homeassistant.helpers.device_registry import DeviceRegistry -from .const import MOCK_VEHICLES +from .const import MOCK_CONFIG, MOCK_VEHICLES from tests.common import MockConfigEntry, load_fixture -async def setup_renault_integration(hass: HomeAssistant): +def get_mock_config_entry(): """Create the Renault integration.""" - config_entry = MockConfigEntry( + return 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", + source=SOURCE_USER, + data=MOCK_CONFIG, + unique_id="account_id_1", options={}, - entry_id="1", + entry_id="123456", ) + + +def get_fixtures(vehicle_type: str) -> dict[str, Any]: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES.get(vehicle_type, {"endpoints": {}}) + return { + "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") + if "battery_status" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).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 load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), + "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") + if "cockpit" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).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 load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), + } + + +async def setup_renault_integration_simple(hass: HomeAssistant): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.renault.RenaultHub.attempt_login", return_value=True - ), patch("homeassistant.components.renault.RenaultHub.async_initialise"): + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): 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 setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: str): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) - -async def create_vehicle_proxy( - hass: HomeAssistant, vehicle_type: str -) -> RenaultVehicleProxy: - """Create a vehicle proxy for testing.""" + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) 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", + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", side_effect=mock_vehicle["endpoints_available"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", return_value=mock_fixtures["battery_status"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", return_value=mock_fixtures["charge_mode"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", return_value=mock_fixtures["cockpit"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", return_value=mock_fixtures["hvac_status"], ): - await vehicle_proxy.async_initialise() - return vehicle_proxy + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry -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] +async def setup_renault_integration_vehicle_with_no_data( + hass: HomeAssistant, vehicle_type: str +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) - 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, + renault_account = RenaultAccount( + config_entry.unique_id, websession=aiohttp_client.async_get_clientsession(hass), ) + mock_vehicle = MOCK_VEHICLES[vehicle_type] + mock_fixtures = get_fixtures("") - vehicle_proxy = RenaultVehicleProxy( - hass, vehicle, vehicle_details, timedelta(seconds=300) - ) - with patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", side_effect=mock_vehicle["endpoints_available"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def setup_renault_integration_vehicle_with_side_effect( + hass: HomeAssistant, vehicle_type: str, side_effect: Any +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) + mock_vehicle = MOCK_VEHICLES[vehicle_type] + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", side_effect=side_effect, ): - await vehicle_proxy.async_initialise() - return vehicle_proxy + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +def check_device_registry( + device_registry: DeviceRegistry, expected_device: dict[str, Any] +) -> None: + """Ensure that the expected_device is correctly registered.""" + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] + assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] + assert registry_entry.name == expected_device[ATTR_NAME] + assert registry_entry.model == expected_device[ATTR_MODEL] + assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION] diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index be2adafd7be..2c742aa07cd 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,4 +1,9 @@ """Constants for the Renault integration tests.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + DOMAIN as BINARY_SENSOR_DOMAIN, +) from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -13,10 +18,14 @@ from homeassistant.const import ( CONF_USERNAME, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TIME_MINUTES, @@ -52,6 +61,20 @@ MOCK_VEHICLES = { "cockpit": "cockpit_ev.json", "hvac_status": "hvac_status.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777999_plugged_in", + "result": STATE_ON, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777999_charging", + "result": STATE_ON, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -59,6 +82,13 @@ MOCK_VEHICLES = { "result": "141", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777999_battery_available_energy", + "result": "31", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", @@ -90,7 +120,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", @@ -138,6 +168,20 @@ MOCK_VEHICLES = { "charge_mode": "charge_mode_schedule.json", "cockpit": "cockpit_ev.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777999_plugged_in", + "result": STATE_OFF, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777999_charging", + "result": STATE_OFF, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -145,6 +189,13 @@ MOCK_VEHICLES = { "result": "128", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777999_battery_available_energy", + "result": "0", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", @@ -176,7 +227,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", @@ -217,6 +268,20 @@ MOCK_VEHICLES = { "charge_mode": "charge_mode_always.json", "cockpit": "cockpit_fuel.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777123_plugged_in", + "result": STATE_ON, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777123_charging", + "result": STATE_ON, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -224,6 +289,13 @@ MOCK_VEHICLES = { "result": "141", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777123_battery_available_energy", + "result": "31", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777123_battery_level", @@ -255,7 +327,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", @@ -304,6 +376,7 @@ MOCK_VEHICLES = { # Ignore, # charge-mode ], "endpoints": {"cockpit": "cockpit_fuel.json"}, + BINARY_SENSOR_DOMAIN: [], SENSOR_DOMAIN: [ { "entity_id": "sensor.fuel_autonomy", diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py new file mode 100644 index 00000000000..71bb90f16a6 --- /dev/null +++ b/tests/components/renault/test_binary_sensor.py @@ -0,0 +1,155 @@ +"""Tests for Renault binary sensors.""" +from unittest.mock import patch + +import pytest +from renault_api.kamereon import exceptions + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from . import ( + check_device_registry, + setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_no_data, + setup_renault_integration_vehicle_with_side_effect, +) +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_binary_sensors(hass, vehicle_type): + """Test for Renault binary sensors.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_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_binary_sensor_empty(hass, vehicle_type): + """Test for Renault binary sensors with empty data from Renault.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_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_OFF + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_binary_sensor_errors(hass, vehicle_type): + """Test for Renault binary 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", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_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_binary_sensor_access_denied(hass): + """Test for Renault binary sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, access_denied_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 + + +async def test_binary_sensor_not_supported(hass): + """Test for Renault binary sensors with not supported failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, not_supported_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index c8b9c8c3e12..684e17a0101 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, PropertyMock, patch from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas +from renault_api.renault_account import RenaultAccount from homeassistant import config_entries, data_entry_flow from homeassistant.components.renault.const import ( @@ -12,126 +13,197 @@ from homeassistant.components.renault.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from . import get_mock_config_entry 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", - }, + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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") + ) ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_credentials"} + # 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", + }, + ) - 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") - ) - ) + 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" - # 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" + assert len(mock_setup_entry.mock_calls) == 1 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", - }, + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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"] == {} - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "kamereon_no_account" + # Account list empty + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + 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" + + assert len(mock_setup_entry.mock_calls) == 0 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"] == {} + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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", - }, + renault_account_1 = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + renault_account_2 = RenaultAccount( + "account_id_2", + websession=aiohttp_client.async_get_clientsession(hass), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "kamereon" + # Multiple accounts + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account_1, renault_account_2], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + 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", + }, + ) - # 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" + 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" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_flow_duplicate(hass: HomeAssistant): + """Test abort if unique_id configured.""" + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + get_mock_config_entry().add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + 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"] == {} + + renault_account = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + 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"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 974155c3df9..37a67151972 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,85 +1,63 @@ """Tests for Renault setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import 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 homeassistant.config_entries import ConfigEntryState -from .const import MOCK_CONFIG - -from tests.common import MockConfigEntry, load_fixture +from . import get_mock_config_entry, setup_renault_integration_simple -async def test_setup_unload_and_reload_entry(hass): +async def test_setup_unload_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("homeassistant.components.renault.PLATFORMS", []): + config_entry = await setup_renault_integration_simple(hass) - 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) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.entry_id in hass.data[DOMAIN] - 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] + # Unload the entry and verify that the data has been removed + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.entry_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 - ) + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) 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) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) 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 - ) + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) # 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) + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 8956fa7e7e6..41fceccb56c 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,17 +1,18 @@ """Tests for Renault sensors.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import 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.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.setup import async_setup_component from . import ( - create_vehicle_proxy, - create_vehicle_proxy_with_side_effect, - setup_renault_integration, + check_device_registry, + setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_no_data, + setup_renault_integration_vehicle_with_side_effect, ) from .const import MOCK_VEHICLES @@ -25,28 +26,12 @@ async def test_sensors(hass, vehicle_type): 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) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle(hass, vehicle_type) 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"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -68,28 +53,12 @@ async def test_sensor_empty(hass, vehicle_type): 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) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) 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"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -101,7 +70,7 @@ async def test_sensor_empty(hass, vehicle_type): 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 + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -116,30 +85,14 @@ async def test_sensor_errors(hass, vehicle_type): "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) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) 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"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -160,26 +113,21 @@ async def test_sensor_access_denied(hass): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + vehicle_type = "zoe_40" 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) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, access_denied_exception + ) await hass.async_block_till_done() - assert len(device_registry.devices) == 0 + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + assert len(entity_registry.entities) == 0 @@ -189,24 +137,19 @@ async def test_sensor_not_supported(hass): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + vehicle_type = "zoe_40" 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) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, not_supported_exception + ) await hass.async_block_till_done() - assert len(device_registry.devices) == 0 + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + assert len(entity_registry.entities) == 0 diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index ce35e2506a9..8e60714a9e2 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -86,7 +86,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 13 + assert len(triggers) == 22 assert triggers == expected_triggers diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py new file mode 100644 index 00000000000..5ff2cad9edc --- /dev/null +++ b/tests/components/sensor/test_init.py @@ -0,0 +1,51 @@ +"""The test for sensor device automation.""" +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + + +async def test_deprecated_temperature_conversion( + hass, caplog, enable_custom_integrations +): + """Test warning on deprecated temperature conversion.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", native_value="0.0", native_unit_of_measurement=TEMP_FAHRENHEIT + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == "-17.8" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ( + "Entity sensor.test () " + "with device_class None reports a temperature in °F which will be converted to " + "°C. Temperature conversion for entities without correct device_class is " + "deprecated and will be removed from Home Assistant Core 2022.3. Please update " + "your configuration if device_class is manually configured, otherwise report it " + "to the custom component author." + ) in caplog.text + + +async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): + """Test warning on deprecated last reset.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", state_class="measurement", last_reset=dt_util.utc_from_timestamp(0) + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "Entity sensor.test () " + "with state_class measurement has set last_reset. Setting last_reset is " + "deprecated and will be unsupported from Home Assistant Core 2021.11. Please " + "update your configuration if state_class is manually configured, otherwise " + "report it to the custom component author." + ) in caplog.text diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 58614e86a0e..3a2572f8141 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -39,6 +39,11 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°C", } +GAS_SENSOR_ATTRIBUTES = { + "device_class": "gas", + "state_class": "measurement", + "unit_of_measurement": "m³", +} @pytest.mark.parametrize( @@ -90,7 +95,6 @@ def test_compile_hourly_statistics( "mean": approx(mean), "min": approx(min), "max": approx(max), - "last_reset": None, "state": None, "sum": None, } @@ -140,7 +144,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "mean": approx(16.440677966101696), "min": approx(10.0), "max": approx(30.0), - "last_reset": None, "state": None, "sum": None, } @@ -154,11 +157,13 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes [ ("energy", "kWh", "kWh", 1), ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "€", "€", 1), + ("monetary", "EUR", "EUR", 1), ("monetary", "SEK", "SEK", 1), + ("gas", "m³", "m³", 1), + ("gas", "ft³", "m³", 0.0283168466), ], ) -def test_compile_hourly_energy_statistics( +def test_compile_hourly_sum_statistics_amount( hass_recorder, caplog, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" @@ -174,7 +179,7 @@ def test_compile_hourly_energy_statistics( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", attributes, seq ) hist = history.get_significant_states( @@ -201,7 +206,6 @@ def test_compile_hourly_energy_statistics( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, @@ -211,9 +215,8 @@ def test_compile_hourly_energy_statistics( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(factor * seq[5]), - "sum": approx(factor * 10.0), + "sum": approx(factor * 30.0), }, { "statistic_id": "sensor.test1", @@ -221,9 +224,85 @@ def test_compile_hourly_energy_statistics( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(factor * seq[8]), - "sum": approx(factor * 40.0), + "sum": approx(factor * 60.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize( + "device_class,unit,native_unit,factor", + [ + ("energy", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", 1 / 1000), + ("gas", "m³", "m³", 1), + ("gas", "ft³", "m³", 0.0283168466), + ], +) +def test_compile_hourly_sum_statistics_total_increasing( + hass_recorder, caplog, device_class, unit, native_unit, factor +): + """Test compiling hourly statistics.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + attributes = { + "device_class": device_class, + "state_class": "total_increasing", + "unit_of_measurement": unit, + } + seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] + + four, eight, states = record_meter_states( + hass, zero, "sensor.test1", attributes, seq + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "state": approx(factor * seq[2]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "state": approx(factor * seq[5]), + "sum": approx(factor * 50.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "state": approx(factor * seq[8]), + "sum": approx(factor * 80.0), }, ] } @@ -254,14 +333,14 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4) states = {**states, **_states} hist = history.get_significant_states( @@ -288,7 +367,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -298,9 +376,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -308,9 +385,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ] } @@ -336,14 +412,14 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4) states = {**states, **_states} hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution @@ -371,7 +447,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -381,9 +456,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -391,9 +465,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ], "sensor.test2": [ @@ -403,7 +476,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(130.0), "sum": approx(20.0), }, @@ -413,9 +485,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(45.0), - "sum": approx(-95.0), + "sum": approx(-65.0), }, { "statistic_id": "sensor.test2", @@ -423,9 +494,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(75.0), - "sum": approx(-65.0), + "sum": approx(-35.0), }, ], "sensor.test3": [ @@ -435,7 +505,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(5.0 / 1000), "sum": approx(5.0 / 1000), }, @@ -445,9 +514,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(50.0 / 1000), - "sum": approx(30.0 / 1000), + "sum": approx(50.0 / 1000), }, { "statistic_id": "sensor.test3", @@ -455,9 +523,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(90.0 / 1000), - "sum": approx(70.0 / 1000), + "sum": approx(90.0 / 1000), }, ], } @@ -508,7 +575,6 @@ def test_compile_hourly_statistics_unchanged( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } @@ -540,7 +606,6 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): "mean": approx(21.1864406779661), "min": approx(10.0), "max": approx(25.0), - "last_reset": None, "state": None, "sum": None, } @@ -597,7 +662,6 @@ def test_compile_hourly_statistics_unavailable( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } @@ -632,6 +696,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("humidity", None, None, "mean"), ("monetary", "USD", "USD", "sum"), ("monetary", "None", "None", "sum"), + ("gas", "m³", "m³", "sum"), + ("gas", "ft³", "m³", "sum"), ("pressure", "Pa", "Pa", "mean"), ("pressure", "hPa", "Pa", "mean"), ("pressure", "mbar", "Pa", "mean"), @@ -697,7 +763,7 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): def record_states(hass, zero, entity_id, attributes): """Record some test states. - We inject a bunch of state updates for temperature sensors. + We inject a bunch of state updates for measurement sensors. """ attributes = dict(attributes) @@ -725,10 +791,10 @@ def record_states(hass, zero, entity_id, attributes): return four, states -def record_energy_states(hass, zero, entity_id, _attributes, seq): +def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states. - We inject a bunch of state updates for energy sensors. + We inject a bunch of state updates for meter sensors. """ def set_state(entity_id, state, **kwargs): diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index 729990ceaeb..e46fbbf8d5e 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -48,9 +48,32 @@ async def test_no_available_tones(hass): process_turn_on_params(siren, {"tone": "test"}) -async def test_missing_tones(hass): - """Test ValueError when setting a tone that is missing from available_tones.""" +async def test_available_tones_list(hass): + """Test that valid tones from tone list will get passed in.""" + siren = MockSirenEntity(SUPPORT_TONES, ["a", "b"]) + siren.hass = hass + assert process_turn_on_params(siren, {"tone": "a"}) == {"tone": "a"} + + +async def test_available_tones_dict(hass): + """Test that valid tones from available_tones dict will get passed in.""" + siren = MockSirenEntity(SUPPORT_TONES, {1: "a", 2: "b"}) + siren.hass = hass + assert process_turn_on_params(siren, {"tone": "a"}) == {"tone": 1} + assert process_turn_on_params(siren, {"tone": 1}) == {"tone": 1} + + +async def test_missing_tones_list(hass): + """Test ValueError when setting a tone that is missing from available_tones list.""" siren = MockSirenEntity(SUPPORT_TONES, ["a", "b"]) siren.hass = hass with pytest.raises(ValueError): process_turn_on_params(siren, {"tone": "test"}) + + +async def test_missing_tones_dict(hass): + """Test ValueError when setting a tone that is missing from available_tones dict.""" + siren = MockSirenEntity(SUPPORT_TONES, {1: "a", 2: "b"}) + siren.hass = hass + with pytest.raises(ValueError): + process_turn_on_params(siren, {"tone": 3}) diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index c6a584802b9..7a7e47f03fa 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -21,15 +21,17 @@ async def test_setup(hass, requests_mock): assert len(devices) == 2 left_side = devices[1] + left_side.hass = hass assert left_side.name == "SleepNumber ILE Test1 SleepNumber" assert left_side.state == 40 right_side = devices[0] + right_side.hass = hass assert right_side.name == "SleepNumber ILE Test2 SleepNumber" assert right_side.state == 80 -async def test_setup_sigle(hass, requests_mock): +async def test_setup_single(hass, requests_mock): """Test for successfully setting up the SleepIQ platform.""" mock_responses(requests_mock, single=True) @@ -41,5 +43,6 @@ async def test_setup_sigle(hass, requests_mock): assert len(devices) == 1 right_side = devices[0] + right_side.hass = hass assert right_side.name == "SleepNumber ILE Test1 SleepNumber" assert right_side.state == 40 diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index e740ea671cd..9460e2235be 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -178,7 +178,7 @@ async def test_discovery(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) assert result["type"] == RESULT_TYPE_FORM @@ -190,7 +190,7 @@ async def test_discovery_no_uuid(hass): with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) assert result["type"] == RESULT_TYPE_FORM @@ -199,9 +199,8 @@ async def test_discovery_no_uuid(hass): async def test_dhcp_discovery(hass): """Test we can process discovery from dhcp.""" - with patch( - "pysqueezebox.Server.async_query", - return_value={"uuid": UUID}, + with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( + "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -216,9 +215,12 @@ async def test_dhcp_discovery(hass): assert result["step_id"] == "edit" -async def test_dhcp_discovery_no_connection(hass): - """Test we can process discovery from dhcp without connecting to squeezebox server.""" - with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): +async def test_dhcp_discovery_no_server_found(hass): + """Test we can handle dhcp discovery when no server is found.""" + with patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_failed_discover, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -229,7 +231,25 @@ async def test_dhcp_discovery_no_connection(hass): }, ) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "edit" + assert result["step_id"] == "user" + + +async def test_dhcp_discovery_existing_player(hass): + """Test that we properly ignore known players during dhcp discover.""" + with patch( + "homeassistant.helpers.entity_registry.EntityRegistry.async_get_entity_id", + return_value="test_entity", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_ABORT async def test_import(hass): diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 069dc9eb64f..3e830b7fc93 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -82,6 +82,7 @@ async def test_srp_entity(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=1.99999999999) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity is not None assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}" @@ -104,6 +105,7 @@ async def test_srp_entity_no_data(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity.extra_state_attributes is None @@ -111,6 +113,7 @@ async def test_srp_entity_no_coord_data(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity.usage is None @@ -124,6 +127,7 @@ async def test_srp_entity_async_update(hass): MagicMock.__await__ = lambda x: async_magic().__await__() fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass await srp_entity.async_update() assert fake_coordinator.async_request_refresh.called diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 34ca1b7228e..2c5dc74db44 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -29,7 +29,13 @@ def _patched_ssdp_listener(info, *args, **kwargs): async def _async_callback(*_): await listener.async_callback(info) + @callback + def _async_search(*_): + # Prevent an actual scan. + pass + listener.async_start = _async_callback + listener.async_search = _async_search return listener @@ -287,7 +293,10 @@ async def test_invalid_characters(hass, aioclient_mock): @patch("homeassistant.components.ssdp.SSDPListener.async_start") @patch("homeassistant.components.ssdp.SSDPListener.async_search") -async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): +@patch("homeassistant.components.ssdp.SSDPListener.async_stop") +async def test_start_stop_scanner( + async_stop_mock, async_search_mock, async_start_mock, hass +): """Test we start and stop the scanner.""" assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) @@ -295,15 +304,18 @@ async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 2 + assert async_start_mock.call_count == 1 + # Next is 2, as async_upnp_client triggers 1 SSDPListener._async_on_connect assert async_search_mock.call_count == 2 + assert async_stop_mock.call_count == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 2 + assert async_start_mock.call_count == 1 assert async_search_mock.call_count == 2 + assert async_stop_mock.call_count == 1 async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog): @@ -787,7 +799,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass): assert argset == { (IPv6Address("2001:db8::"), None), - (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), (IPv4Address("192.168.1.5"), None), } @@ -802,12 +813,12 @@ async def test_bind_failure_skips_adapter(hass, caplog): ] } create_args = [] - did_search = 0 + search_args = [] @callback - def _callback(*_): - nonlocal did_search - did_search += 1 + def _callback(*args): + nonlocal search_args + search_args.append(args) pass def _generate_failing_ssdp_listener(*args, **kwargs): @@ -844,11 +855,74 @@ async def test_bind_failure_skips_adapter(hass, caplog): 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 + assert set(search_args) == { + (), + ( + ( + "255.255.255.255", + 1900, + ), + ), + } + + +async def test_ipv4_does_additional_search_for_sonos(hass, caplog): + """Test that only ipv4 does an additional search for Sonos.""" + mock_get_ssdp = { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + } + search_args = [] + + def _generate_fake_ssdp_listener(*args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + pass + + @callback + def _callback(*args): + nonlocal search_args + search_args.append(args) + 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_fake_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() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + + assert set(search_args) == { + (), + ( + ( + "255.255.255.255", + 1900, + ), + ), + } diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index ffbeb44d79e..e62a190d7be 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -15,6 +15,7 @@ failure modes or corner cases like how out of order packets are handled. import fractions import io +import logging import math import threading from unittest.mock import patch @@ -52,7 +53,7 @@ SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION TIMEOUT = 15 -class FakePyAvStream: +class FakeAvInputStream: """A fake pyav Stream.""" def __init__(self, name, rate): @@ -66,9 +67,13 @@ class FakePyAvStream: self.codec = FakeCodec() + def __str__(self) -> str: + """Return a stream name for debugging.""" + return f"FakePyAvStream<{self.name}, {self.time_base}>" -VIDEO_STREAM = FakePyAvStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) -AUDIO_STREAM = FakePyAvStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) + +VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) +AUDIO_STREAM = FakeAvInputStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) class PacketSequence: @@ -110,6 +115,9 @@ class PacketSequence: is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) size = 3 + def __str__(self) -> str: + return f"FakePacket" + return FakePacket() @@ -154,7 +162,7 @@ class FakePyAvBuffer: def add_stream(self, template=None): """Create an output buffer that captures packets for test to examine.""" - class FakeStream: + class FakeAvOutputStream: def __init__(self, capture_packets): self.capture_packets = capture_packets @@ -162,11 +170,15 @@ class FakePyAvBuffer: return def mux(self, packet): + logging.debug("Muxed packet: %s", packet) self.capture_packets.append(packet) + def __str__(self) -> str: + return f"FakeAvOutputStream<{template.name}>" + if template.name == AUDIO_STREAM_FORMAT: - return FakeStream(self.audio_packets) - return FakeStream(self.video_packets) + return FakeAvOutputStream(self.audio_packets) + return FakeAvOutputStream(self.video_packets) def mux(self, packet): """Capture a packet for tests to examine.""" @@ -217,7 +229,7 @@ async def async_decode_stream(hass, packets, py_av=None): if not py_av: py_av = MockPyAv() - py_av.container.packets = packets + py_av.container.packets = iter(packets) # Can't be rewound with patch("av.open", new=py_av.open), patch( "homeassistant.components.stream.core.StreamOutput.put", @@ -273,7 +285,7 @@ async def test_skip_out_of_order_packet(hass): assert not packets[out_of_order_index].is_keyframe packets[out_of_order_index].dts = -9090 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -309,7 +321,7 @@ async def test_discard_old_packets(hass): # Packets after this one are considered out of order packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments @@ -331,7 +343,7 @@ async def test_packet_overflow(hass): # Packet is so far out of order, exceeds max gap and looks like overflow packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments @@ -355,7 +367,7 @@ async def test_skip_initial_bad_packets(hass): for i in range(0, num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -385,7 +397,7 @@ async def test_too_many_initial_bad_packets_fails(hass): for i in range(0, num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments assert len(segments) == 0 assert len(decoded_stream.video_packets) == 0 @@ -405,7 +417,7 @@ async def test_skip_missing_dts(hass): continue packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -426,7 +438,7 @@ async def test_too_many_bad_packets(hass): for i in range(bad_packet_start, bad_packet_start + num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) complete_segments = decoded_stream.complete_segments assert len(complete_segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == bad_packet_start @@ -454,7 +466,7 @@ async def test_audio_packets_not_found(hass): num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 packets = PacketSequence(num_packets) # Contains only video packets - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments assert len(complete_segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets @@ -474,8 +486,10 @@ async def test_adts_aac_audio(hass): packets[1][0] = 255 packets[1][1] = 241 - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) assert len(decoded_stream.audio_packets) == 0 + # All decoded video packets are still preserved + assert len(decoded_stream.video_packets) == num_packets - 1 async def test_audio_is_first_packet(hass): @@ -493,7 +507,7 @@ async def test_audio_is_first_packet(hass): packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments # The audio packets are segmented with the video packets assert len(complete_segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) @@ -511,7 +525,7 @@ async def test_audio_packets_found(hass): packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments # The audio packet above is buffered with the video packet assert len(complete_segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) @@ -529,7 +543,7 @@ async def test_pts_out_of_order(hass): packets[i].pts = packets[i - 1].pts - 1 packets[i].is_keyframe = False - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 9f8d821e74b..2ccfb26d3ef 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -2,9 +2,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -12,6 +9,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index fc1e7fd624b..adb73dcf334 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -255,6 +255,9 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert ( + state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_TOTAL_INCREASING + ) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("sensor.tasmota_energy_total") @@ -269,7 +272,6 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "1.2" - assert state.attributes["last_reset"] == "2018-11-23T15:33:47+00:00" # Test polled state update async_fire_mqtt_message( @@ -279,7 +281,6 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "5.6" - assert state.attributes["last_reset"] == "2018-11-23T16:33:47+00:00" async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py new file mode 100644 index 00000000000..4e2f5e0ff09 --- /dev/null +++ b/tests/components/traccar/test_device_tracker.py @@ -0,0 +1,62 @@ +"""The tests for the Traccar device tracker platform.""" +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from homeassistant.components.device_tracker.const import DOMAIN +from homeassistant.components.traccar.device_tracker import ( + PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, +) +from homeassistant.const import ( + CONF_EVENT, + CONF_HOST, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_USERNAME, +) +from homeassistant.setup import async_setup_component + +from tests.common import async_capture_events + + +async def test_import_events_catch_all(hass): + """Test importing all events and firing them in HA using their event types.""" + conf_dict = { + DOMAIN: TRACCAR_PLATFORM_SCHEMA( + { + CONF_PLATFORM: "traccar", + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + CONF_EVENT: ["all_events"], + } + ) + } + + device = {"id": 1, "name": "abc123"} + api_mock = AsyncMock() + api_mock.devices = [device] + api_mock.get_events.return_value = [ + { + "deviceId": device["id"], + "type": "ignitionOn", + "serverTime": datetime.utcnow(), + "attributes": {}, + }, + { + "deviceId": device["id"], + "type": "ignitionOff", + "serverTime": datetime.utcnow(), + "attributes": {}, + }, + ] + + events_ignition_on = async_capture_events(hass, "traccar_ignition_on") + events_ignition_off = async_capture_events(hass, "traccar_ignition_off") + + with patch( + "homeassistant.components.traccar.device_tracker.API", return_value=api_mock + ): + assert await async_setup_component(hass, DOMAIN, conf_dict) + + assert len(events_ignition_on) == 1 + assert len(events_ignition_off) == 1 diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 080aadb2bc7..7ccfdc63a34 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -7,6 +7,8 @@ from homeassistant import config_entries, setup from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + USER_INPUT = { "email": "test-email@example.com", "password": "test-password", @@ -76,3 +78,165 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: + """Test user input for config_entry that already exists.""" + first_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + first_entry.add_to_hass(hass) + + with patch("aiotractive.api.API.user_id", return_value="USERID"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_reauthentication(hass): + """Test Tractive reauthentication.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch("aiotractive.api.API.user_id", return_value="USERID"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauthentication_failure(hass): + """Test Tractive reauthentication failure.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "aiotractive.api.API.user_id", + side_effect=aiotractive.exceptions.UnauthorizedError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "invalid_auth" + + +async def test_reauthentication_unknown_failure(hass): + """Test Tractive reauthentication failure.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "aiotractive.api.API.user_id", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "unknown" + + +async def test_reauthentication_failure_no_existing_entry(hass): + """Test Tractive reauthentication with no existing entry.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch("aiotractive.api.API.user_id", return_value="USERID_DIFFERENT"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_failed_existing" diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 8e11ab06f34..e8cc83a456c 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,81 +1,12 @@ """Tests for Tradfri setup.""" from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components import tradfri from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_config_yaml_host_not_imported(hass): - """Test that we don't import a configured host.""" - MockConfigEntry(domain="tradfri", data={"host": "mock-host"}).add_to_hass(hass) - - with patch( - "homeassistant.components.tradfri.load_json", return_value={} - ), patch.object(hass.config_entries.flow, "async_init") as mock_init: - assert await async_setup_component( - hass, "tradfri", {"tradfri": {"host": "mock-host"}} - ) - await hass.async_block_till_done() - - assert len(mock_init.mock_calls) == 0 - - -async def test_config_yaml_host_imported(hass): - """Test that we import a configured host.""" - with patch("homeassistant.components.tradfri.load_json", return_value={}): - assert await async_setup_component( - hass, "tradfri", {"tradfri": {"host": "mock-host"}} - ) - await hass.async_block_till_done() - - progress = hass.config_entries.flow.async_progress() - assert len(progress) == 1 - assert progress[0]["handler"] == "tradfri" - assert progress[0]["context"] == {"source": config_entries.SOURCE_IMPORT} - - -async def test_config_json_host_not_imported(hass): - """Test that we don't import a configured host.""" - MockConfigEntry(domain="tradfri", data={"host": "mock-host"}).add_to_hass(hass) - - with patch( - "homeassistant.components.tradfri.load_json", - return_value={"mock-host": {"key": "some-info"}}, - ), patch.object(hass.config_entries.flow, "async_init") as mock_init: - assert await async_setup_component(hass, "tradfri", {"tradfri": {}}) - await hass.async_block_till_done() - - assert len(mock_init.mock_calls) == 0 - - -async def test_config_json_host_imported( - hass, mock_gateway_info, mock_entry_setup, gateway_id -): - """Test that we import a configured host.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: { - "host": host, - "identity": identity, - "key": key, - "gateway_id": gateway_id, - } - - with patch( - "homeassistant.components.tradfri.load_json", - return_value={"mock-host": {"key": "some-info"}}, - ): - assert await async_setup_component(hass, "tradfri", {"tradfri": {}}) - await hass.async_block_till_done() - - config_entry = mock_entry_setup.mock_calls[0][1][1] - assert config_entry.domain == "tradfri" - assert config_entry.source == config_entries.SOURCE_IMPORT - assert config_entry.title == "mock-host" - - async def test_entry_setup_unload(hass, api_factory, gateway_id): """Test config entry setup and unload.""" entry = MockConfigEntry( diff --git a/tests/components/upnp/common.py b/tests/components/upnp/common.py new file mode 100644 index 00000000000..4dd0fd4083d --- /dev/null +++ b/tests/components/upnp/common.py @@ -0,0 +1,23 @@ +"""Common for upnp.""" + +from urllib.parse import urlparse + +from homeassistant.components import ssdp + +TEST_UDN = "uuid:device" +TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" +TEST_USN = f"{TEST_UDN}::{TEST_ST}" +TEST_LOCATION = "http://192.168.1.1/desc.xml" +TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_FRIENDLY_NAME = "friendly name" +TEST_DISCOVERY = { + ssdp.ATTR_SSDP_LOCATION: TEST_LOCATION, + ssdp.ATTR_SSDP_ST: TEST_ST, + ssdp.ATTR_SSDP_USN: TEST_USN, + ssdp.ATTR_UPNP_UDN: TEST_UDN, + "usn": TEST_USN, + "location": TEST_LOCATION, + "_host": TEST_HOSTNAME, + "_udn": TEST_UDN, + "friendlyName": TEST_FRIENDLY_NAME, +} diff --git a/tests/components/upnp/mock_ssdp_scanner.py b/tests/components/upnp/mock_ssdp_scanner.py new file mode 100644 index 00000000000..39f9a801bb6 --- /dev/null +++ b/tests/components/upnp/mock_ssdp_scanner.py @@ -0,0 +1,49 @@ +"""Mock ssdp.Scanner.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components import ssdp +from homeassistant.core import callback + + +class MockSsdpDescriptionManager(ssdp.DescriptionManager): + """Mocked ssdp DescriptionManager.""" + + async def fetch_description( + self, xml_location: str | None + ) -> None | dict[str, str]: + """Fetch the location or get it from the cache.""" + if xml_location is None: + return None + return {} + + +class MockSsdpScanner(ssdp.Scanner): + """Mocked ssdp Scanner.""" + + @callback + def async_stop(self, *_: Any) -> None: + """Stop the scanner.""" + # Do nothing. + + async def async_start(self) -> None: + """Start the scanner.""" + self.description_manager = MockSsdpDescriptionManager(self.hass) + + @callback + def async_scan(self, *_: Any) -> None: + """Scan for new entries.""" + # Do nothing. + + +@pytest.fixture +def mock_ssdp_scanner(): + """Mock ssdp Scanner.""" + with patch( + "homeassistant.components.ssdp.Scanner", new=MockSsdpScanner + ) as mock_ssdp_scanner: + yield mock_ssdp_scanner diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_upnp_device.py similarity index 63% rename from tests/components/upnp/mock_device.py rename to tests/components/upnp/mock_upnp_device.py index 7161ae69598..42c9291f30f 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_upnp_device.py @@ -1,7 +1,9 @@ """Mock device for testing purposes.""" from typing import Any, Mapping -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch + +import pytest from homeassistant.components.upnp.const import ( BYTES_RECEIVED, @@ -9,10 +11,15 @@ from homeassistant.components.upnp.const import ( PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, + UPTIME, + WANIP, + WANSTATUS, ) from homeassistant.components.upnp.device import Device from homeassistant.util import dt +from .common import TEST_UDN + class MockDevice(Device): """Mock device for Device.""" @@ -23,12 +30,13 @@ class MockDevice(Device): mock_device_updater = AsyncMock() super().__init__(igd_device, mock_device_updater) self._udn = udn - self.times_polled = 0 + self.traffic_times_polled = 0 + self.status_times_polled = 0 @classmethod async def async_create_device(cls, hass, ssdp_location) -> "MockDevice": """Return self.""" - return cls("UDN") + return cls(TEST_UDN) @property def udn(self) -> str: @@ -62,7 +70,7 @@ class MockDevice(Device): async def async_get_traffic_data(self) -> Mapping[str, Any]: """Get traffic data.""" - self.times_polled += 1 + self.traffic_times_polled += 1 return { TIMESTAMP: dt.utcnow(), BYTES_RECEIVED: 0, @@ -70,3 +78,27 @@ class MockDevice(Device): PACKETS_RECEIVED: 0, PACKETS_SENT: 0, } + + async def async_get_status(self) -> Mapping[str, Any]: + """Get connection status, uptime, and external IP.""" + self.status_times_polled += 1 + return { + WANSTATUS: "Connected", + UPTIME: 0, + WANIP: "192.168.0.1", + } + + async def async_start(self) -> None: + """Start the device updater.""" + + async def async_stop(self) -> None: + """Stop the device updater.""" + + +@pytest.fixture +def mock_upnp_device(): + """Mock upnp Device.async_create_device.""" + with patch( + "homeassistant.components.upnp.Device", new=MockDevice + ) as mock_async_create_device: + yield mock_async_create_device diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 6e546be93f3..907fa709c84 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,8 +1,9 @@ """Test UPnP/IGD config flow.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch -from urllib.parse import urlparse +from unittest.mock import patch + +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -12,119 +13,91 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, ) -from homeassistant.components.upnp.device import Device -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt -from .mock_device import MockDevice +from .common import ( + TEST_DISCOVERY, + TEST_FRIENDLY_NAME, + TEST_HOSTNAME, + TEST_LOCATION, + TEST_ST, + TEST_UDN, + TEST_USN, +) +from .mock_ssdp_scanner import mock_ssdp_scanner # noqa: F401 +from .mock_upnp_device import mock_upnp_device # noqa: F401 from tests.common import MockConfigEntry, async_fire_time_changed -async def test_flow_ssdp_discovery(hass: HomeAssistant): +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") +async def test_flow_ssdp_discovery( + hass: HomeAssistant, +): """Test config flow: discovered + configured through ssdp.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step ssdp. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "ssdp_confirm" - - # Confirm via step ssdp_confirm. - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } - - -async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): - """Test config flow: incomplete discovery through ssdp.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + + # Confirm via step ssdp_confirm. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } + + +@pytest.mark.usefixtures("mock_ssdp_scanner") +async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): + """Test config flow: incomplete discovery through ssdp.""" # Discovered via step ssdp. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. + ssdp.ATTR_SSDP_LOCATION: TEST_LOCATION, + ssdp.ATTR_SSDP_ST: TEST_ST, + ssdp.ATTR_SSDP_USN: TEST_USN, + # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_discovery" +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): """Test config flow: discovery through ssdp, but ignored, as hostname is used by existing config entry.""" - udn = "uuid:device_random_1" - location = "http://dummy" - mock_device = MockDevice(udn) - # Existing entry. config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: "uuid:device_random_2", - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: urlparse(location).hostname, + CONFIG_ENTRY_UDN: TEST_UDN + "2", + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) @@ -134,129 +107,78 @@ async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - }, + data=TEST_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "discovery_ignored" +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_flow_user(hass: HomeAssistant): """Test config flow: discovered + configured through user.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step user. - 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["step_id"] == "user" + # Discovered via step user. + 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["step_id"] == "user" - # Confirmed via step user. - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"unique_id": mock_device.unique_id}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } + # Confirmed via step user. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"unique_id": TEST_USN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_flow_import(hass: HomeAssistant): - """Test config flow: discovered + configured through configuration.yaml.""" - udn = "uuid:device_1" - mock_device = MockDevice(udn) - location = "http://dummy" - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] + """Test config flow: configured through configuration.yaml.""" + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } + # Discovered via step import. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_import_already_configured(hass: HomeAssistant): - """Test config flow: discovered, but already configured.""" - udn = "uuid:device_1" - mock_device = MockDevice(udn) - + """Test config flow: configured through configuration.yaml, but existing config entry.""" # Existing entry. config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) @@ -271,94 +193,93 @@ async def test_flow_import_already_configured(hass: HomeAssistant): assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_import_no_devices_found(hass: HomeAssistant): """Test config flow: no devices found, configured through configuration.yaml.""" - ssdp_discoveries = [] - with patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache.clear() + + # Discovered via step import. + with patch( + "homeassistant.components.upnp.config_flow.SSDP_SEARCH_TIMEOUT", new=0.0 ): - # Discovered via step import. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_options_flow(hass: HomeAssistant): """Test options flow.""" + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running + # Set up config entry. - udn = "uuid:device_1" - location = "http://192.168.1.1/desc.xml" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + mock_device = hass.data[DOMAIN][config_entry.entry_id].device - config = { - # no upnp, ensures no import-flow is started. + # Reset. + mock_device.traffic_times_polled = 0 + mock_device.status_times_polled = 0 + + # Forward time, ensure single poll after 30 (default) seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + assert mock_device.traffic_times_polled == 1 + assert mock_device.status_times_polled == 1 + + # Options flow with no input results in form. + result = await hass.config_entries.options.async_init( + config_entry.entry_id, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Options flow with input results in update to entry. + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONFIG_ENTRY_SCAN_INTERVAL: 60, } - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, - "async_get_discovery_info_by_udn_st", - Mock(return_value=ssdp_discoveries[0]), - ): - # Initialisation of component. - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() - mock_device.times_polled = 0 # Reset. - # Forward time, ensure single poll after 30 (default) seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() - assert mock_device.times_polled == 1 + # Forward time, ensure single poll after 60 seconds, still from original setting. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + assert mock_device.traffic_times_polled == 2 + assert mock_device.status_times_polled == 2 - # Options flow with no input results in form. - result = await hass.config_entries.options.async_init( - config_entry.entry_id, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + # Now the updated interval takes effect. + # Forward time, ensure single poll after 120 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) + await hass.async_block_till_done() + assert mock_device.traffic_times_polled == 3 + assert mock_device.status_times_polled == 3 - # Options flow with input results in update to entry. - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, - ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == { - CONFIG_ENTRY_SCAN_INTERVAL: 60, - } - - # Forward time, ensure single poll after 60 seconds, still from original setting. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) - await hass.async_block_till_done() - assert mock_device.times_polled == 2 - - # Now the updated interval takes effect. - # Forward time, ensure single poll after 120 seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) - await hass.async_block_till_done() - assert mock_device.times_polled == 3 - - # Forward time, ensure single poll after 180 seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) - await hass.async_block_till_done() - assert mock_device.times_polled == 4 + # Forward time, ensure single poll after 180 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) + await hass.async_block_till_done() + assert mock_device.traffic_times_polled == 4 + assert mock_device.status_times_polled == 4 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 0770906f0da..9ccdbf02f4b 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,6 +1,7 @@ """Test UPnP/IGD setup process.""" +from __future__ import annotations -from unittest.mock import AsyncMock, Mock, patch +import pytest from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( @@ -8,51 +9,37 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_UDN, DOMAIN, ) -from homeassistant.components.upnp.device import Device -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component -from .mock_device import MockDevice +from .common import TEST_DISCOVERY, TEST_ST, TEST_UDN +from .mock_ssdp_scanner import mock_ssdp_scanner # noqa: F401 +from .mock_upnp_device import mock_upnp_device # noqa: F401 from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_async_setup_entry_default(hass: HomeAssistant): """Test async_setup_entry.""" - udn = "uuid:device_1" - location = "http://192.168.1.1/desc.xml" - mock_device = MockDevice(udn) - discovery = { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, }, ) - config = { - # no upnp - } - async_create_device = AsyncMock(return_value=mock_device) - mock_get_discovery = Mock() - with patch.object(Device, "async_create_device", async_create_device), patch.object( - ssdp, "async_get_discovery_info_by_udn_st", mock_get_discovery - ): - # initialisation of component, no device discovered - mock_get_discovery.return_value = None - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() + # Initialisation of component, no device discovered. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - # loading of config_entry, device discovered - mock_get_discovery.return_value = discovery - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) is True + # Device is discovered. + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - # ensure device is stored/used - async_create_device.assert_called_with(hass, discovery[ssdp.ATTR_SSDP_LOCATION]) + # Load config_entry. + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py new file mode 100644 index 00000000000..aa241ce5a92 --- /dev/null +++ b/tests/components/uptimerobot/common.py @@ -0,0 +1,95 @@ +"""Common constants and functions for Uptime Robot tests.""" +from __future__ import annotations + +from enum import Enum +from typing import Any +from unittest.mock import patch + +from pyuptimerobot import ( + APIStatus, + UptimeRobotAccount, + UptimeRobotApiError, + UptimeRobotApiResponse, + UptimeRobotMonitor, +) + +from homeassistant import config_entries +from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_UPTIMEROBOT_API_KEY = "1234" +MOCK_UPTIMEROBOT_UNIQUE_ID = "1234567890" + +MOCK_UPTIMEROBOT_ACCOUNT = {"email": "test@test.test", "user_id": 1234567890} +MOCK_UPTIMEROBOT_ERROR = {"message": "test error from API."} +MOCK_UPTIMEROBOT_MONITOR = { + "id": 1234, + "friendly_name": "Test monitor", + "status": 2, + "type": 1, + "url": "http://example.com", +} + +MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { + "domain": DOMAIN, + "title": "test@test.test", + "data": {"platform": DOMAIN, "api_key": MOCK_UPTIMEROBOT_API_KEY}, + "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, + "source": config_entries.SOURCE_USER, +} + +UPTIMEROBOT_TEST_ENTITY = "binary_sensor.test_monitor" + + +class MockApiResponseKey(str, Enum): + """Mock API response key.""" + + ACCOUNT = "account" + ERROR = "error" + MONITORS = "monitors" + + +def mock_uptimerobot_api_response( + data: dict[str, Any] + | None + | list[UptimeRobotMonitor] + | UptimeRobotAccount + | UptimeRobotApiError = None, + status: APIStatus = APIStatus.OK, + key: MockApiResponseKey = MockApiResponseKey.MONITORS, +) -> UptimeRobotApiResponse: + """Mock API response for Uptime Robot.""" + return UptimeRobotApiResponse.from_dict( + { + "stat": {"error": APIStatus.FAIL}.get(key, status), + key: data + if data is not None + else { + "account": MOCK_UPTIMEROBOT_ACCOUNT, + "error": MOCK_UPTIMEROBOT_ERROR, + "monitors": [MOCK_UPTIMEROBOT_MONITOR], + }.get(key, {}), + } + ) + + +async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Uptime Robot integration.""" + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert mock_entry.state == config_entries.ConfigEntryState.LOADED + + return mock_entry diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py new file mode 100644 index 00000000000..13bb3b342e9 --- /dev/null +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -0,0 +1,82 @@ +"""Test Uptime Robot binary_sensor.""" + +from unittest.mock import patch + +from pyuptimerobot import UptimeRobotAuthenticationException + +from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.uptimerobot.const import ( + ATTRIBUTION, + COORDINATOR_UPDATE_INTERVAL, + DOMAIN, +) +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .common import ( + MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_MONITOR, + UPTIMEROBOT_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + +from tests.common import async_fire_time_changed + + +async def test_config_import(hass: HomeAssistant) -> None: + """Test importing YAML configuration.""" + config = { + "binary_sensor": { + "platform": DOMAIN, + "api_key": MOCK_UPTIMEROBOT_API_KEY, + } + } + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(config_entries) == 1 + config_entry = config_entries[0] + assert config_entry.source == "import" + + +async def test_presentation(hass: HomeAssistant) -> None: + """Test the presenstation of Uptime Robot binary_sensors.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + + assert entity.state == STATE_ON + assert entity.attributes["device_class"] == DEVICE_CLASS_CONNECTIVITY + assert entity.attributes["attribution"] == ATTRIBUTION + assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] + + +async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + assert entity.state == STATE_UNAVAILABLE diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 41f0b6b639e..966483970d0 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -1,15 +1,13 @@ """Test the Uptime Robot config flow.""" from unittest.mock import patch +import pytest from pytest import LogCaptureFixture -from pyuptimerobot import UptimeRobotApiResponse -from pyuptimerobot.exceptions import ( - UptimeRobotAuthenticationException, - UptimeRobotException, -) +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException from homeassistant import config_entries, setup from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -17,6 +15,15 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) +from .common import ( + MOCK_UPTIMEROBOT_ACCOUNT, + MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_UNIQUE_ID, + MockApiResponseKey, + mock_uptimerobot_api_response, +) + from tests.common import MockConfigEntry @@ -31,82 +38,49 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() - assert result2["result"].unique_id == "1234567890" + assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "test@test.test" - assert result2["data"] == {"api_key": "1234"} + assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] + assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" +@pytest.mark.parametrize( + "exception,error_key", + [ + (Exception, "unknown"), + (UptimeRobotException, "cannot_connect"), + (UptimeRobotAuthenticationException, "invalid_api_key"), + ], +) +async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) -> None: + """Test that we handle exceptions.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=UptimeRobotException, + side_effect=exception, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_unexpected_error(hass: HomeAssistant) -> None: - """Test we handle unexpected error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"api_key": "1234"}, - ) - - assert result2["errors"] == {"base": "unknown"} - - -async def test_form_api_key_error(hass: HomeAssistant) -> None: - """Test we handle unexpected error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=UptimeRobotAuthenticationException, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"api_key": "1234"}, - ) - - assert result2["errors"] == {"base": "invalid_api_key"} + assert result2["errors"]["base"] == error_key async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: @@ -117,32 +91,24 @@ async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "fail", - "error": {"message": "test error from API."}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"]["base"] == "unknown" assert "test error from API." in caplog.text -async def test_flow_import(hass): +async def test_flow_import( + hass: HomeAssistant, +) -> None: """Test an import flow.""" with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -150,22 +116,17 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "1234"}, + data={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {"api_key": "1234"} + assert result["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -173,7 +134,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "1234"}, + data={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() @@ -183,7 +144,9 @@ async def test_flow_import(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict({"stat": "ok"}), + return_value=mock_uptimerobot_api_response( + key=MockApiResponseKey.ACCOUNT, data={} + ), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -191,7 +154,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "12345"}, + data={"platform": DOMAIN, CONF_API_KEY: "12345"}, ) await hass.async_block_till_done() @@ -199,39 +162,195 @@ async def test_flow_import(hass): assert result["reason"] == "unknown" -async def test_user_unique_id_already_exists(hass): +async def test_user_unique_id_already_exists( + hass: HomeAssistant, +) -> None: """Test creating an entry where the unique_id already exists.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - ) + entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "12345"}, + {CONF_API_KEY: "12345"}, ) await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == "abort" + assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" + + +async def test_reauthentication( + hass: HomeAssistant, +) -> None: + """Test Uptime Robot reauthentication.""" + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_reauthentication_failure( + hass: HomeAssistant, +) -> None: + """Test Uptime Robot reauthentication failure.""" + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "unknown" + + +async def test_reauthentication_failure_no_existing_entry( + hass: HomeAssistant, +) -> None: + """Test Uptime Robot reauthentication with no existing entry.""" + old_entry = MockConfigEntry( + **{**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, "unique_id": None} + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_failed_existing" + + +async def test_reauthentication_failure_account_not_matching( + hass: HomeAssistant, +) -> None: + """Test Uptime Robot reauthentication failure when using another account.""" + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response( + key=MockApiResponseKey.ACCOUNT, + data={**MOCK_UPTIMEROBOT_ACCOUNT, "user_id": 1234567891}, + ), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py new file mode 100644 index 00000000000..43f78e7a19f --- /dev/null +++ b/tests/components/uptimerobot/test_init.py @@ -0,0 +1,187 @@ +"""Test the Uptime Robot init.""" +from unittest.mock import patch + +from pytest import LogCaptureFixture +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException + +from homeassistant import config_entries +from homeassistant.components.uptimerobot.const import ( + COORDINATOR_UPDATE_INTERVAL, + DOMAIN, +) +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get_registry, +) +from homeassistant.util import dt + +from .common import ( + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_MONITOR, + UPTIMEROBOT_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_reauthentication_trigger_in_setup( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_config_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.reason == "could not authenticate" + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + assert ( + "Config entry 'test@test.test' for uptimerobot integration could not authenticate" + in caplog.text + ) + + +async def test_reauthentication_trigger_after_setup( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = await setup_uptimerobot_integration(hass) + + binary_sensor = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert binary_sensor.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + + assert "Authentication failed while fetching uptimerobot data" in caplog.text + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + +async def test_integration_reload(hass: HomeAssistant): + """Test integration reload.""" + mock_entry = await setup_uptimerobot_integration(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + assert await hass.config_entries.async_reload(mock_entry.entry_id) + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state == config_entries.ConfigEntryState.LOADED + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + + +async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): + """Test errors during updates.""" + await setup_uptimerobot_integration(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotException, + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + + assert "Error fetching uptimerobot data: test error from API" in caplog.text + + +async def test_device_management(hass: HomeAssistant): + """Test that we are adding and removing devices for monitors returned from the API.""" + mock_entry = await setup_uptimerobot_integration(hass) + dev_reg = await async_get_registry(hass) + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 1 + + assert devices[0].identifiers == {(DOMAIN, "1234")} + assert devices[0].name == "Test monitor" + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response( + data=[MOCK_UPTIMEROBOT_MONITOR, {**MOCK_UPTIMEROBOT_MONITOR, "id": 12345}] + ), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 2 + assert devices[0].identifiers == {(DOMAIN, "1234")} + assert devices[1].identifiers == {(DOMAIN, "12345")} + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2").state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 1 + assert devices[0].identifiers == {(DOMAIN, "1234")} + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 164b4090e5f..c8883e72389 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,26 +1,100 @@ """The test for the version sensor platform.""" +from datetime import timedelta from unittest.mock import patch +from pyhaversion import HaVersionSource, exceptions as pyhaversionexceptions +import pytest + +from homeassistant.components.version.sensor import HA_VERSION_SOURCES from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from tests.common import async_fire_time_changed MOCK_VERSION = "10.0" -async def test_version_sensor(hass): - """Test the Version sensor.""" - config = {"sensor": {"platform": "version"}} +@pytest.mark.parametrize( + "source,target_source,name", + ( + ( + ("local", HaVersionSource.LOCAL, "current_version"), + ("docker", HaVersionSource.CONTAINER, "latest_version"), + ("hassio", HaVersionSource.SUPERVISOR, "latest_version"), + ) + + tuple( + (source, HaVersionSource(source), "latest_version") + for source in HA_VERSION_SOURCES + if source != HaVersionSource.LOCAL + ) + ), +) +async def test_version_source(hass, source, target_source, name): + """Test the Version sensor with different sources.""" + config = { + "sensor": {"platform": "version", "source": source, "image": "qemux86-64"} + } - assert await async_setup_component(hass, "sensor", config) - - -async def test_version(hass): - """Test the Version sensor.""" - config = {"sensor": {"platform": "version", "name": "test"}} - - with patch("homeassistant.const.__version__", MOCK_VERSION): + with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( + "homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION + ): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - state = hass.states.get("sensor.test") + state = hass.states.get(f"sensor.{name}") + assert state + assert state.attributes["source"] == target_source - assert state.state == "10.0" + assert state.state == MOCK_VERSION + + +async def test_version_fetch_exception(hass, caplog): + """Test fetch exception thrown during updates.""" + config = {"sensor": {"platform": "version"}} + with patch( + "homeassistant.components.version.sensor.HaVersion.get_version", + side_effect=pyhaversionexceptions.HaVersionFetchException( + "Fetch exception from pyhaversion" + ), + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + assert "Fetch exception from pyhaversion" in caplog.text + + +async def test_version_parse_exception(hass, caplog): + """Test parse exception thrown during updates.""" + config = {"sensor": {"platform": "version"}} + with patch( + "homeassistant.components.version.sensor.HaVersion.get_version", + side_effect=pyhaversionexceptions.HaVersionParseException, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + assert "Could not parse data received for HaVersionSource.LOCAL" in caplog.text + + +async def test_update(hass): + """Test updates.""" + config = {"sensor": {"platform": "version"}} + + with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( + "homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.current_version") + assert state + assert state.state == MOCK_VERSION + + with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( + "homeassistant.components.version.sensor.HaVersion.version", "1234" + ): + + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + + state = hass.states.get("sensor.current_version") + assert state + assert state.state == "1234" diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 4449859ddb2..e1dbda1dd04 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -29,6 +29,7 @@ class TestVultrSensorSetup(unittest.TestCase): def add_entities(self, devices, action): """Mock add devices.""" for device in devices: + device.hass = self.hass self.DEVICES.append(device) def setUp(self): diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 7766fe512cc..bf69318706c 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -22,8 +22,8 @@ def pywemo_model_fixture(): return "LightSwitch" -@pytest.fixture(name="pywemo_registry") -def pywemo_registry_fixture(): +@pytest.fixture(name="pywemo_registry", autouse=True) +async def async_pywemo_registry_fixture(): """Fixture for SubscriptionRegistry instances.""" registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) @@ -40,6 +40,13 @@ def pywemo_registry_fixture(): yield registry +@pytest.fixture(name="pywemo_discovery_responder", autouse=True) +def pywemo_discovery_responder_fixture(): + """Fixture for the DiscoveryResponder instance.""" + with patch("pywemo.ssdp.DiscoveryResponder", autospec=True): + yield + + @pytest.fixture(name="pywemo_device") def pywemo_device_fixture(pywemo_registry, pywemo_model): """Fixture for WeMoDevice instances.""" diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index bbb56efdeda..f1c96bc3ed8 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -35,6 +35,9 @@ async def test_setup(hass, requests_mock): def add_entities(new_entities, update_before_add=False): """Mock add entities.""" + for entity in new_entities: + entity.hass = hass + if update_before_add: for entity in new_entities: entity.update() diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 5725880f942..cb2936cf8e2 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,9 +1,13 @@ """Tests for the Yeelight integration.""" -from unittest.mock import MagicMock, patch +import asyncio +from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, patch +from async_upnp_client.search import SSDPListener from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS +from homeassistant.components import yeelight as hass_yeelight from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -13,6 +17,7 @@ from homeassistant.components.yeelight import ( YeelightScanner, ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME +from homeassistant.core import callback IP_ADDRESS = "192.168.1.239" MODEL = "color" @@ -23,13 +28,16 @@ CAPABILITIES = { "id": ID, "model": MODEL, "fw_ver": FW_VER, + "location": f"yeelight://{IP_ADDRESS}", "support": "get_prop set_default set_power toggle set_bright start_cf stop_cf" " set_scene cron_add cron_get cron_del set_ct_abx set_rgb", "name": "", } NAME = "name" -UNIQUE_NAME = f"yeelight_{MODEL}_{ID}" +SHORT_ID = hex(int("0x000000000015243f", 16)) +UNIQUE_NAME = f"yeelight_{MODEL}_{SHORT_ID}" +UNIQUE_FRIENDLY_NAME = f"Yeelight {MODEL.title()} {SHORT_ID}" MODULE = "homeassistant.components.yeelight" MODULE_CONFIG_FLOW = f"{MODULE}.config_flow" @@ -81,29 +89,77 @@ CONFIG_ENTRY_DATA = {CONF_ID: ID} def _mocked_bulb(cannot_connect=False): bulb = MagicMock() - type(bulb).get_capabilities = MagicMock( - return_value=None if cannot_connect else CAPABILITIES + type(bulb).async_listen = AsyncMock( + side_effect=BulbException if cannot_connect else None + ) + type(bulb).async_get_properties = AsyncMock( + side_effect=BulbException if cannot_connect else None ) type(bulb).get_properties = MagicMock( side_effect=BulbException if cannot_connect else None ) type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL]) - bulb.capabilities = CAPABILITIES + bulb.capabilities = CAPABILITIES.copy() bulb.model = MODEL bulb.bulb_type = BulbType.Color - bulb.last_properties = PROPERTIES + bulb.last_properties = PROPERTIES.copy() bulb.music_mode = False + bulb.async_get_properties = AsyncMock() + bulb.async_stop_listening = AsyncMock() + bulb.async_update = AsyncMock() + bulb.async_turn_on = AsyncMock() + bulb.async_turn_off = AsyncMock() + bulb.async_set_brightness = AsyncMock() + bulb.async_set_color_temp = AsyncMock() + bulb.async_set_hsv = AsyncMock() + bulb.async_set_rgb = AsyncMock() + bulb.async_start_flow = AsyncMock() + bulb.async_stop_flow = AsyncMock() + bulb.async_set_power_mode = AsyncMock() + bulb.async_set_scene = AsyncMock() + bulb.async_set_default = AsyncMock() return bulb -def _patch_discovery(prefix, no_device=False): +def _patched_ssdp_listener(info, *args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + await listener.async_connect_callback() + + @callback + def _async_search(*_): + if info: + asyncio.create_task(listener.async_callback(info)) + + listener.async_start = _async_callback + listener.async_search = _async_search + return listener + + +def _patch_discovery(no_device=False): YeelightScanner._scanner = None # Clear class scanner to reset hass - def _mocked_discovery(timeout=2, interface=False): - if no_device: - return [] - return [{"ip": IP_ADDRESS, "port": 55443, "capabilities": CAPABILITIES}] + def _generate_fake_ssdp_listener(*args, **kwargs): + return _patched_ssdp_listener( + None if no_device else CAPABILITIES, + *args, + **kwargs, + ) - return patch(f"{prefix}.discover_bulbs", side_effect=_mocked_discovery) + return patch( + "homeassistant.components.yeelight.SSDPListener", + new=_generate_fake_ssdp_listener, + ) + + +def _patch_discovery_interval(): + return patch.object( + hass_yeelight, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0) + ) + + +def _patch_discovery_timeout(): + return patch.object(hass_yeelight, "DISCOVERY_TIMEOUT", 0.0001) diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index f716469fc9a..350c289f5b5 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -6,7 +6,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component from homeassistant.setup import async_setup_component -from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb +from . import ( + MODULE, + NAME, + PROPERTIES, + YAML_CONFIGURATION, + _mocked_bulb, + _patch_discovery, +) ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" @@ -14,9 +21,7 @@ ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" async def test_nightlight(hass: HomeAssistant): """Test nightlight sensor.""" mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) await hass.async_block_till_done() diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8994c8e3360..5bbfcc9283b 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Yeelight config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -25,14 +25,17 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( + CAPABILITIES, ID, IP_ADDRESS, MODULE, MODULE_CONFIG_FLOW, NAME, - UNIQUE_NAME, + UNIQUE_FRIENDLY_NAME, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, + _patch_discovery_timeout, ) from tests.common import MockConfigEntry @@ -55,21 +58,23 @@ async def test_discovery(hass: HomeAssistant): assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "form" assert result2["step_id"] == "pick_device" assert not result2["errors"] - with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( f"{MODULE}.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) assert result3["type"] == "create_entry" - assert result3["title"] == UNIQUE_NAME - assert result3["data"] == {CONF_ID: ID} + assert result3["title"] == UNIQUE_FRIENDLY_NAME + assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS} await hass.async_block_till_done() mock_setup.assert_called_once() mock_setup_entry.assert_called_once() @@ -82,7 +87,7 @@ async def test_discovery(hass: HomeAssistant): assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" @@ -94,7 +99,9 @@ async def test_discovery_no_device(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" @@ -114,26 +121,27 @@ async def test_import(hass: HomeAssistant): # Cannot connect mocked_bulb = _mocked_bulb(cannot_connect=True) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - type(mocked_bulb).get_capabilities.assert_called_once() - type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "abort" assert result["reason"] == "cannot_connect" # Success mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( + with _patch_discovery(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( f"{MODULE}.async_setup_entry", return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - type(mocked_bulb).get_capabilities.assert_called_once() assert result["type"] == "create_entry" assert result["title"] == DEFAULT_NAME assert result["data"] == { @@ -150,7 +158,9 @@ async def test_import(hass: HomeAssistant): # Duplicate mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) @@ -169,7 +179,11 @@ async def test_manual(hass: HomeAssistant): # Cannot connect (timeout) mocked_bulb = _mocked_bulb(cannot_connect=True) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -178,8 +192,11 @@ async def test_manual(hass: HomeAssistant): assert result2["errors"] == {"base": "cannot_connect"} # Cannot connect (error) - type(mocked_bulb).get_capabilities = MagicMock(side_effect=OSError) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -187,9 +204,11 @@ async def test_manual(hass: HomeAssistant): # Success mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with _patch_discovery(), _patch_discovery_timeout(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), patch(f"{MODULE}.async_setup", return_value=True), patch( + f"{MODULE}.async_setup_entry", return_value=True + ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -203,7 +222,11 @@ async def test_manual(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -219,7 +242,7 @@ async def test_options(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -241,7 +264,7 @@ async def test_options(hass: HomeAssistant): config[CONF_NIGHTLIGHT_SWITCH] = True user_input = {**config} user_input.pop(CONF_NAME) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input ) @@ -262,15 +285,18 @@ async def test_manual_no_capabilities(hass: HomeAssistant): assert not result["errors"] mocked_bulb = _mocked_bulb() - type(mocked_bulb).get_capabilities = MagicMock(return_value=None) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), patch( f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + ), patch( + f"{MODULE}.async_setup_entry", return_value=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - type(mocked_bulb).get_capabilities.assert_called_once() - type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "create_entry" assert result["data"] == {CONF_HOST: IP_ADDRESS} @@ -280,39 +306,53 @@ async def test_discovered_by_homekit_and_dhcp(hass): await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + data={"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"}, + data={"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, ) + await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + data={"ip": IP_ADDRESS, "macaddress": "00:00:00:00:00:00"}, ) + await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "already_in_progress" - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", side_effect=CannotConnect): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"}, ) + await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "cannot_connect" @@ -335,17 +375,25 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( f"{MODULE}.async_setup_entry", return_value=True ) as mock_async_setup_entry: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "create_entry" assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"} assert mock_async_setup.called @@ -370,10 +418,55 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - type(mocked_bulb).get_capabilities = MagicMock(return_value=None) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" + + +async def test_discovered_ssdp(hass): + """Test we can setup when discovered from ssdp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 2d1113d1896..d7f4a05b436 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,7 +1,8 @@ """Test Yeelight.""" -from unittest.mock import MagicMock, patch +from datetime import timedelta +from unittest.mock import AsyncMock, patch -from yeelight import BulbType +from yeelight import BulbException, BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, @@ -22,9 +23,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import ( - CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, @@ -34,12 +35,14 @@ from . import ( ID, IP_ADDRESS, MODULE, - MODULE_CONFIG_FLOW, + SHORT_ID, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, + _patch_discovery_timeout, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_ip_changes_fallback_discovery(hass: HomeAssistant): @@ -51,34 +54,41 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.get_capabilities = MagicMock( - side_effect=[OSError, CAPABILITIES, CAPABILITIES] - ) + mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) - _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.discover_bulbs", return_value=_discovered_devices - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await hass.async_block_till_done() binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( - f"yeelight_color_{ID}" + f"yeelight_color_{SHORT_ID}" ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None - await hass.async_block_till_done() + type(mocked_bulb).async_get_properties = AsyncMock(None) - type(mocked_bulb).get_properties = MagicMock(None) - - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update() await hass.async_block_till_done() await hass.async_block_till_done() entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): + # The discovery should update the ip address + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + # Make sure we can still reload with the new ip right after we change it + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get(binary_sensor_entity_id) is not None + async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): """Test Yeelight ip changes and we fallback to discovery.""" @@ -87,11 +97,9 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.get_capabilities = MagicMock( - side_effect=[OSError, CAPABILITIES, CAPABILITIES] - ) + mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -104,7 +112,7 @@ async def test_setup_discovery(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -127,9 +135,7 @@ async def test_setup_import(hass: HomeAssistant): """Test import from yaml.""" mocked_bulb = _mocked_bulb() name = "yeelight" - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( - f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): assert await async_setup_component( hass, DOMAIN, @@ -162,7 +168,7 @@ async def test_unique_ids_device(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -186,7 +192,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -216,24 +222,57 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( - IP_ADDRESS.replace(".", "_") + assert config_entry.state is ConfigEntryState.LOADED + + +async def test_async_listen_error_late_discovery(hass, caplog): + """Test the async listen error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(cannot_connect=True) + + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + assert "Failed to connect to bulb at" in caplog.text + + +async def test_async_listen_error_has_host_with_id(hass: HomeAssistant): + """Test the async listen error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"} ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None + config_entry.add_to_hass(hass) - type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) - type(mocked_bulb).get_properties = MagicMock(None) + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + ): + await hass.config_entries.async_setup(config_entry.entry_id) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() - await hass.async_block_till_done() - await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is not None + +async def test_async_listen_error_has_host_without_id(hass: HomeAssistant): + """Test the async listen error but no id.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}) + config_entry.add_to_hass(hass) + + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + ): + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9283514cb70..7497fa8773e 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,6 +1,6 @@ """Test the Yeelight light.""" import logging -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from yeelight import ( BulbException, @@ -19,6 +19,7 @@ from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, @@ -101,9 +102,10 @@ from . import ( MODULE, NAME, PROPERTIES, - UNIQUE_NAME, + UNIQUE_FRIENDLY_NAME, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, ) from tests.common import MockConfigEntry @@ -131,7 +133,9 @@ async def test_services(hass: HomeAssistant, caplog): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -146,8 +150,11 @@ async def test_services(hass: HomeAssistant, caplog): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success - mocked_method = MagicMock() - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock() + else: + mocked_method = MagicMock() + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() @@ -161,8 +168,11 @@ async def test_services(hass: HomeAssistant, caplog): # failure if failure_side_effect: - mocked_method = MagicMock(side_effect=failure_side_effect) - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock(side_effect=failure_side_effect) + else: + mocked_method = MagicMock(side_effect=failure_side_effect) + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) @@ -173,6 +183,7 @@ async def test_services(hass: HomeAssistant, caplog): brightness = 100 rgb_color = (0, 128, 255) transition = 2 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -186,30 +197,30 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_called_once_with( *rgb_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on hs_color brightness = 100 @@ -228,35 +239,36 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_called_once_with( *hs_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on color_temp brightness = 100 color_temp = 200 transition = 1 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -270,31 +282,32 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_color_temp.assert_called_once_with( + mocked_bulb.async_set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.last_properties["power"] = "off" # turn_on nightlight await _async_test_service( SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT}, - "turn_on", + "async_turn_on", payload={ "duration": DEFAULT_TRANSITION, "light_type": LightType.Main, @@ -303,11 +316,12 @@ async def test_services(hass: HomeAssistant, caplog): domain="light", ) + mocked_bulb.last_properties["power"] = "on" # turn_off await _async_test_service( SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, - "turn_off", + "async_turn_off", domain="light", payload={"duration": transition * 1000, "light_type": LightType.Main}, ) @@ -317,7 +331,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"}, - "set_power_mode", + "async_set_power_mode", [PowerMode[mode.upper()]], ) @@ -328,7 +342,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "start_flow", + "async_start_flow", ) # set_color_scene @@ -339,7 +353,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_RGB_COLOR: [10, 20, 30], ATTR_BRIGHTNESS: 50, }, - "set_scene", + "async_set_scene", [SceneClass.COLOR, 10, 20, 30, 50], ) @@ -347,7 +361,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_HSV_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.HSV, 180, 50, 50], ) @@ -355,7 +369,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_COLOR_TEMP_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.CT, 4000, 50], ) @@ -366,14 +380,14 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "set_scene", + "async_set_scene", ) # set_auto_delay_off_scene await _async_test_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.AUTO_DELAY_OFF, 50, 1], ) @@ -401,6 +415,7 @@ async def test_services(hass: HomeAssistant, caplog): failure_side_effect=None, ) # test _cmd wrapper error handler + mocked_bulb.last_properties["power"] = "off" err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException) @@ -415,6 +430,115 @@ async def test_services(hass: HomeAssistant, caplog): ) +async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): + """Ensure we suppress state changes that will increase the rate limit when there is no change.""" + mocked_bulb = _mocked_bulb() + properties = {**PROPERTIES} + properties.pop("active_mode") + properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties = properties + mocked_bulb.bulb_type = BulbType.Color + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} + ) + config_entry.add_to_hass(hass) + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_HS_COLOR: (PROPERTIES["hue"], PROPERTIES["sat"]), + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 1 + rgb = int(PROPERTIES["rgb"]) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_rgb.reset_mock() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS_PCT: PROPERTIES["current_brightness"], + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + # Should call for the color mode change + assert mocked_bulb.async_set_color_temp.mock_calls == [ + call(4000, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_color_temp.reset_mock() + + mocked_bulb.last_properties["color_mode"] = 2 + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 3 + # This last change should generate a call even though + # the color mode is the same since the HSV has changed + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (5, 5)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [ + call(5.0, 5.0, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + async def test_device_types(hass: HomeAssistant, caplog): """Test different device types.""" mocked_bulb = _mocked_bulb() @@ -424,8 +548,11 @@ async def test_device_types(hass: HomeAssistant, caplog): mocked_bulb.last_properties = properties async def _async_setup(config_entry): - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): - await hass.config_entries.async_setup(config_entry.entry_id) + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again await hass.async_block_till_done() async def _async_test( @@ -433,7 +560,7 @@ async def test_device_types(hass: HomeAssistant, caplog): model, target_properties, nightlight_properties=None, - name=UNIQUE_NAME, + name=UNIQUE_FRIENDLY_NAME, entity_id=ENTITY_LIGHT, ): config_entry = MockConfigEntry( @@ -447,6 +574,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await _async_setup(config_entry) state = hass.states.get(entity_id) + assert state.state == "on" target_properties["friendly_name"] = name target_properties["flowing"] = False @@ -471,7 +599,7 @@ async def test_device_types(hass: HomeAssistant, caplog): assert hass.states.get(entity_id).state == "off" state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" - nightlight_properties["friendly_name"] = f"{name} nightlight" + nightlight_properties["friendly_name"] = f"{name} Nightlight" nightlight_properties["icon"] = "mdi:weather-night" nightlight_properties["flowing"] = False nightlight_properties["night_light"] = True @@ -481,6 +609,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry.async_clear_config_entry(config_entry.entry_id) + await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) @@ -765,7 +894,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -786,7 +915,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -807,7 +936,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -841,7 +970,9 @@ async def test_effects(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -850,8 +981,8 @@ async def test_effects(hass: HomeAssistant): ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] async def _async_test_effect(name, target=None, called=True): - mocked_start_flow = MagicMock() - type(mocked_bulb).start_flow = mocked_start_flow + async_mocked_start_flow = AsyncMock() + mocked_bulb.async_start_flow = async_mocked_start_flow await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -860,10 +991,10 @@ async def test_effects(hass: HomeAssistant): ) if not called: return - mocked_start_flow.assert_called_once() + async_mocked_start_flow.assert_called_once() if target is None: return - args, _ = mocked_start_flow.call_args + args, _ = async_mocked_start_flow.call_args flow = args[0] assert flow.count == target.count assert flow.action == target.action diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 3b8cf883a13..a284c91e4f4 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}], @@ -790,17 +794,19 @@ 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) await hass.async_block_till_done() assert mock_zc.mock_calls[0] == call( - interfaces=[1, "192.168.1.5", "172.16.1.5", 3], ip_version=IPVersion.All + interfaces=[ + "2001:db8::", + "fe80::1234:5678:9abc:def0", + "192.168.1.5", + "172.16.1.5", + "fe80::dead:beef:dead:beef", + ], + ip_version=IPVersion.All, ) @@ -823,3 +829,46 @@ 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", "fe80::dead:beef:dead:beef"], + ip_version=IPVersion.All, + ) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 4a777fcebb6..49fa11de26c 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -8,14 +8,11 @@ import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.zha import DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_coro +from tests.common import async_get_device_automations, async_mock_service, mock_coro from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 SHORT_PRESS = "remote_button_short_press" diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index ae0fa44ed8c..4f995131d15 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -70,8 +70,10 @@ def test_get_device_detects_battery_sensor(mock_openzwave): assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY -def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): +def test_multilevelsensor_value_changed_temp_fahrenheit(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" + hass.config.units.temperature_unit = homeassistant.const.TEMP_FAHRENHEIT + node = MockNode( command_classes=[ const.COMMAND_CLASS_SENSOR_MULTILEVEL, @@ -82,6 +84,7 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 191.0 assert device.unit_of_measurement == homeassistant.const.TEMP_FAHRENHEIT assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE @@ -90,8 +93,9 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): assert device.state == 198.0 -def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): +def test_multilevelsensor_value_changed_temp_celsius(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" + hass.config.units.temperature_unit = homeassistant.const.TEMP_CELSIUS node = MockNode( command_classes=[ const.COMMAND_CLASS_SENSOR_MULTILEVEL, @@ -102,6 +106,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 38.9 assert device.unit_of_measurement == homeassistant.const.TEMP_CELSIUS assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE @@ -110,7 +115,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): assert device.state == 38.0 -def test_multilevelsensor_value_changed_other_units(mock_openzwave): +def test_multilevelsensor_value_changed_other_units(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for other units.""" node = MockNode( command_classes=[ @@ -124,6 +129,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 190.96 assert device.unit_of_measurement == homeassistant.const.ENERGY_KILO_WATT_HOUR assert device.device_class is None @@ -132,7 +138,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): assert device.state == 197.96 -def test_multilevelsensor_value_changed_integer(mock_openzwave): +def test_multilevelsensor_value_changed_integer(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for other units.""" node = MockNode( command_classes=[ @@ -144,6 +150,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 5 assert device.unit_of_measurement == "counts" assert device.device_class is None @@ -152,7 +159,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): assert device.state == 6 -def test_alarm_sensor_value_changed(mock_openzwave): +def test_alarm_sensor_value_changed(hass, mock_openzwave): """Test value changed for Z-Wave sensor.""" node = MockNode( command_classes=[const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM] @@ -161,6 +168,7 @@ def test_alarm_sensor_value_changed(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 12.34 assert device.unit_of_measurement == homeassistant.const.PERCENTAGE assert device.device_class is None diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 44943fed9fb..2590149c462 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,6 +1,4 @@ """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" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" @@ -16,7 +14,7 @@ NOTIFICATION_MOTION_BINARY_SENSOR = ( ) NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value" -BASIC_SENSOR = "sensor.livingroomlight_basic" +BASIC_NUMBER_ENTITY = "number.livingroomlight_basic" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) @@ -35,6 +33,3 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" 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/conftest.py b/tests/components/zwave_js/conftest.py index 75b5ab65d38..900a7937539 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -11,11 +11,6 @@ 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 @@ -452,6 +447,14 @@ def aeotec_zw164_siren_state_fixture(): return json.loads(load_fixture("zwave_js/aeotec_zw164_siren_state.json")) +@pytest.fixture(name="lock_popp_electric_strike_lock_control_state", scope="session") +def lock_popp_electric_strike_lock_control_state_fixture(): + """Load the popp electric strike lock control node state fixture data.""" + return json.loads( + load_fixture("zwave_js/lock_popp_electric_strike_lock_control_state.json") + ) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -830,26 +833,23 @@ def ge_in_wall_dimmer_switch_fixture(client, ge_in_wall_dimmer_switch_state): @pytest.fixture(name="aeotec_zw164_siren") def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): - """Mock a wallmote central scene node.""" + """Mock a aeotec zw164 siren node.""" node = Node(client, copy.deepcopy(aeotec_zw164_siren_state)) client.driver.controller.nodes[node.node_id] = node return node +@pytest.fixture(name="lock_popp_electric_strike_lock_control") +def lock_popp_electric_strike_lock_control_fixture( + client, lock_popp_electric_strike_lock_control_state +): + """Mock a popp electric strike lock control node.""" + node = Node(client, copy.deepcopy(lock_popp_electric_strike_lock_control_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="firmware_file") 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_cover.py b/tests/components/zwave_js/test_cover.py index 70ce2337abf..1afe7a114da 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -23,7 +23,7 @@ from homeassistant.const import ( WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" -SHUTTER_COVER_ENTITY = "cover.flush_shutter_dc" +SHUTTER_COVER_ENTITY = "cover.flush_shutter" AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index eef672c4c5b..0256981a726 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -430,7 +430,18 @@ async def test_get_condition_capabilities_value( ) assert capabilities and "extra_fields" in capabilities - cc_options = [(cc.value, cc.name) for cc in CommandClass] + cc_options = [ + (133, "ASSOCIATION"), + (128, "BATTERY"), + (112, "CONFIGURATION"), + (98, "DOOR_LOCK"), + (122, "FIRMWARE_UPDATE_MD"), + (114, "MANUFACTURER_SPECIFIC"), + (113, "ALARM"), + (152, "SECURITY"), + (99, "USER_CODE"), + (134, "VERSION"), + ] assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 8914019cd43..9758d3b0f44 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -57,6 +57,17 @@ async def test_vision_security_zl7432( assert state.attributes["assumed_state"] +async def test_lock_popp_electric_strike_lock_control( + hass, client, lock_popp_electric_strike_lock_control, integration +): + """Test that the Popp Electric Strike Lock Control gets discovered correctly.""" + assert hass.states.get("lock.node_62") is not None + assert ( + hass.states.get("binary_sensor.node_62_the_current_status_of_the_door") + is not None + ) + + async def test_firmware_version_range_exception(hass): """Test FirmwareVersionRange exception.""" with pytest.raises(ValueError): diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index fa3c73a9a42..5ce66d6d8e2 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -223,58 +223,23 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - warm_args = client.async_send_command.call_args_list[0][0][0] # red 255 - assert warm_args["command"] == "node.set_value" - assert warm_args["nodeId"] == 39 - assert warm_args["valueId"]["commandClassName"] == "Color Switch" - assert warm_args["valueId"]["commandClass"] == 51 - assert warm_args["valueId"]["endpoint"] == 0 - assert warm_args["valueId"]["metadata"]["label"] == "Target value (Red)" - assert warm_args["valueId"]["property"] == "targetColor" - assert warm_args["valueId"]["propertyName"] == "targetColor" - assert warm_args["value"] == 255 - - cold_args = client.async_send_command.call_args_list[1][0][0] # green 76 - assert cold_args["command"] == "node.set_value" - assert cold_args["nodeId"] == 39 - assert cold_args["valueId"]["commandClassName"] == "Color Switch" - assert cold_args["valueId"]["commandClass"] == 51 - assert cold_args["valueId"]["endpoint"] == 0 - assert cold_args["valueId"]["metadata"]["label"] == "Target value (Green)" - assert cold_args["valueId"]["property"] == "targetColor" - assert cold_args["valueId"]["propertyName"] == "targetColor" - assert cold_args["value"] == 76 - red_args = client.async_send_command.call_args_list[2][0][0] # blue 255 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 255 - green_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 - assert green_args["command"] == "node.set_value" - assert green_args["nodeId"] == 39 - assert green_args["valueId"]["commandClassName"] == "Color Switch" - assert green_args["valueId"]["commandClass"] == 51 - assert green_args["valueId"]["endpoint"] == 0 - assert green_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" - assert green_args["valueId"]["property"] == "targetColor" - assert green_args["valueId"]["propertyName"] == "targetColor" - assert green_args["value"] == 0 - blue_args = client.async_send_command.call_args_list[4][0][0] # cold white 0 - assert blue_args["command"] == "node.set_value" - assert blue_args["nodeId"] == 39 - assert blue_args["valueId"]["commandClassName"] == "Color Switch" - assert blue_args["valueId"]["commandClass"] == 51 - assert blue_args["valueId"]["endpoint"] == 0 - assert blue_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" - assert blue_args["valueId"]["property"] == "targetColor" - assert blue_args["valueId"]["propertyName"] == "targetColor" - assert blue_args["value"] == 0 + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"]["commandClassName"] == "Color Switch" + assert args["valueId"]["commandClass"] == 51 + assert args["valueId"]["endpoint"] == 0 + assert args["valueId"]["metadata"]["label"] == "Target Color" + assert args["valueId"]["property"] == "targetColor" + assert args["valueId"]["propertyName"] == "targetColor" + assert args["value"] == { + "blue": 255, + "coldWhite": 0, + "green": 76, + "red": 255, + "warmWhite": 0, + } # Test rgb color update from value updated event red_event = Event( @@ -328,7 +293,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 + assert len(client.async_send_command.call_args_list) == 2 client.async_send_command.reset_mock() @@ -344,8 +309,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - args = client.async_send_command.call_args_list[5][0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] assert args["options"]["transitionDuration"] == "20s" client.async_send_command.reset_mock() @@ -357,57 +322,23 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - red_args = client.async_send_command.call_args_list[0][0][0] # red 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[1][0][0] # green 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Green)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[2][0][0] # blue 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - warm_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 - assert warm_args["command"] == "node.set_value" - assert warm_args["nodeId"] == 39 - assert warm_args["valueId"]["commandClassName"] == "Color Switch" - assert warm_args["valueId"]["commandClass"] == 51 - assert warm_args["valueId"]["endpoint"] == 0 - assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" - assert warm_args["valueId"]["property"] == "targetColor" - assert warm_args["valueId"]["propertyName"] == "targetColor" - assert warm_args["value"] == 20 - red_args = client.async_send_command.call_args_list[4][0][0] # cold white - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 235 + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] # red 0 + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"]["commandClassName"] == "Color Switch" + assert args["valueId"]["commandClass"] == 51 + assert args["valueId"]["endpoint"] == 0 + assert args["valueId"]["metadata"]["label"] == "Target Color" + assert args["valueId"]["property"] == "targetColor" + assert args["valueId"]["propertyName"] == "targetColor" + assert args["value"] == { + "blue": 0, + "coldWhite": 235, + "green": 0, + "red": 0, + "warmWhite": 20, + } client.async_send_command.reset_mock() @@ -466,7 +397,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 + assert len(client.async_send_command.call_args_list) == 2 client.async_send_command.reset_mock() @@ -482,8 +413,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - args = client.async_send_command.call_args_list[5][0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] assert args["options"]["transitionDuration"] == "35s" client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index b7d83068bea..6d9458d096c 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -1,7 +1,13 @@ """Test the Z-Wave JS number platform.""" from zwave_js_server.event import Event +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er + +from .common import BASIC_NUMBER_ENTITY + NUMBER_ENTITY = "number.thermostat_hvac_valve_control" +VOLUME_NUMBER_ENTITY = "number.indoor_siren_6_default_volume_2" async def test_number(hass, client, aeotec_radiator_thermostat, integration): @@ -67,3 +73,105 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): state = hass.states.get(NUMBER_ENTITY) assert state.state == "99.0" + + +async def test_volume_number(hass, client, aeotec_zw164_siren, integration): + """Test the volume number entity.""" + node = aeotec_zw164_siren + state = hass.states.get(VOLUME_NUMBER_ENTITY) + + assert state + assert state.state == "1.0" + assert state.attributes["step"] == 0.01 + assert state.attributes["max"] == 1.0 + assert state.attributes["min"] == 0 + + # Test turn on setting value + await hass.services.async_call( + "number", + "set_value", + {"entity_id": VOLUME_NUMBER_ENTITY, "value": 0.3}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%", + }, + "value": 100, + } + assert args["value"] == 30 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultVolume", + "newValue": 30, + "prevValue": 100, + "propertyName": "defaultVolume", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(VOLUME_NUMBER_ENTITY) + assert state.state == "0.3" + + # Test null value + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultVolume", + "newValue": None, + "prevValue": 30, + "propertyName": "defaultVolume", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(VOLUME_NUMBER_ENTITY) + assert state.state == STATE_UNKNOWN + + +async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration): + """Test number is created from Basic CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(BASIC_NUMBER_ENTITY) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py new file mode 100644 index 00000000000..43f44f0bba0 --- /dev/null +++ b/tests/components/zwave_js/test_select.py @@ -0,0 +1,201 @@ +"""Test the Z-Wave JS number platform.""" +from zwave_js_server.event import Event + +from homeassistant.const import STATE_UNKNOWN + +DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2" +PROTECTION_SELECT_ENTITY = "select.family_room_combo_local_protection_state" + + +async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration): + """Test the default tone select entity.""" + node = aeotec_zw164_siren + state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) + + assert state + assert state.state == "17ALAR~1 (35 sec)" + attr = state.attributes + assert attr["options"] == [ + "01DING~1 (5 sec)", + "02DING~1 (9 sec)", + "03TRAD~1 (11 sec)", + "04ELEC~1 (2 sec)", + "05WEST~1 (13 sec)", + "06CHIM~1 (7 sec)", + "07CUCK~1 (31 sec)", + "08TRAD~1 (6 sec)", + "09SMOK~1 (11 sec)", + "10SMOK~1 (6 sec)", + "11FIRE~1 (35 sec)", + "12COSE~1 (5 sec)", + "13KLAX~1 (38 sec)", + "14DEEP~1 (41 sec)", + "15WARN~1 (37 sec)", + "16TORN~1 (46 sec)", + "17ALAR~1 (35 sec)", + "18DEEP~1 (62 sec)", + "19ALAR~1 (15 sec)", + "20ALAR~1 (7 sec)", + "21DIGI~1 (8 sec)", + "22ALER~1 (64 sec)", + "23SHIP~1 (4 sec)", + "25CHRI~1 (4 sec)", + "26GONG~1 (12 sec)", + "27SING~1 (1 sec)", + "28TONA~1 (5 sec)", + "29UPWA~1 (2 sec)", + "30DOOR~1 (27 sec)", + ] + + # Test select option with string value + await hass.services.async_call( + "select", + "select_option", + {"entity_id": DEFAULT_TONE_SELECT_ENTITY, "option": "30DOOR~1 (27 sec)"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Default tone ID", + "min": 0, + "max": 254, + }, + "value": 17, + } + assert args["value"] == 30 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultToneId", + "newValue": 30, + "prevValue": 17, + "propertyName": "defaultToneId", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) + assert state.state == "30DOOR~1 (27 sec)" + + +async def test_protection_select(hass, client, inovelli_lzw36, integration): + """Test the default tone select entity.""" + node = inovelli_lzw36 + state = hass.states.get(PROTECTION_SELECT_ENTITY) + + assert state + assert state.state == "Unprotected" + attr = state.attributes + assert attr["options"] == [ + "Unprotected", + "ProtectedBySequence", + "NoOperationPossible", + ] + + # Test select option with string value + await hass.services.async_call( + "select", + "select_option", + {"entity_id": PROTECTION_SELECT_ENTITY, "option": "ProtectedBySequence"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "1": "ProtectedBySequence", + "2": "NoOperationPossible", + }, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "newValue": 1, + "prevValue": 0, + "propertyName": "local", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state.state == "ProtectedBySequence" + + # Test null value + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "newValue": None, + "prevValue": 1, + "propertyName": "local", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 04583559421..6d64f6f92dd 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,9 +1,10 @@ """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, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -28,16 +29,12 @@ 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, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, METER_ENERGY_SENSOR, - METER_VOLTAGE_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, VOLTAGE_SENSOR, @@ -77,7 +74,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 + assert state.attributes["state_class"] == STATE_CLASS_TOTAL_INCREASING state = hass.states.get(VOLTAGE_SENSOR) @@ -131,16 +128,6 @@ async def test_disabled_indcator_sensor( 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) @@ -203,31 +190,14 @@ 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_ENERGY_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_ENERGY_SENSOR, - }, - blocking=True, - ) - - assert ( - hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] - == DATETIME_LAST_RESET.isoformat() + # Test successful meter reset call + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, + }, + blocking=True, ) assert len(client.async_send_command_no_wait.call_args_list) == 1 @@ -237,10 +207,6 @@ 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 @@ -262,26 +228,4 @@ 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() - - -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_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 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/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 937b2c0fa67..ebe437eb981 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -2,6 +2,7 @@ from zwave_js_server.event import Event from homeassistant.components.siren import ATTR_TONE, ATTR_VOLUME_LEVEL +from homeassistant.components.siren.const import ATTR_AVAILABLE_TONES from homeassistant.const import STATE_OFF, STATE_ON SIREN_ENTITY = "siren.indoor_siren_6_2" @@ -65,6 +66,39 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): assert state assert state.state == STATE_OFF + assert state.attributes.get(ATTR_AVAILABLE_TONES) == { + 0: "off", + 1: "01DING~1 (5 sec)", + 2: "02DING~1 (9 sec)", + 3: "03TRAD~1 (11 sec)", + 4: "04ELEC~1 (2 sec)", + 5: "05WEST~1 (13 sec)", + 6: "06CHIM~1 (7 sec)", + 7: "07CUCK~1 (31 sec)", + 8: "08TRAD~1 (6 sec)", + 9: "09SMOK~1 (11 sec)", + 10: "10SMOK~1 (6 sec)", + 11: "11FIRE~1 (35 sec)", + 12: "12COSE~1 (5 sec)", + 13: "13KLAX~1 (38 sec)", + 14: "14DEEP~1 (41 sec)", + 15: "15WARN~1 (37 sec)", + 16: "16TORN~1 (46 sec)", + 17: "17ALAR~1 (35 sec)", + 18: "18DEEP~1 (62 sec)", + 19: "19ALAR~1 (15 sec)", + 20: "20ALAR~1 (7 sec)", + 21: "21DIGI~1 (8 sec)", + 22: "22ALER~1 (64 sec)", + 23: "23SHIP~1 (4 sec)", + 25: "25CHRI~1 (4 sec)", + 26: "26GONG~1 (12 sec)", + 27: "27SING~1 (1 sec)", + 28: "28TONA~1 (5 sec)", + 29: "29UPWA~1 (2 sec)", + 30: "30DOOR~1 (27 sec)", + 255: "default", + } # Test turn on with default await hass.services.async_call( @@ -105,6 +139,28 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): client.async_send_command.reset_mock() + # Test turn on with specific tone ID and volume level + await hass.services.async_call( + "siren", + "turn_on", + { + "entity_id": SIREN_ENTITY, + ATTR_TONE: 1, + ATTR_VOLUME_LEVEL: 0.5, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == TONE_ID_VALUE_ID + assert args["value"] == 1 + assert args["options"] == {"volume": 50} + + client.async_send_command.reset_mock() + # Test turn off await hass.services.async_call( "siren", diff --git a/tests/fixtures/homekit_controller/arlo_baby.json b/tests/fixtures/homekit_controller/arlo_baby.json new file mode 100644 index 00000000000..6a124a5f56f --- /dev/null +++ b/tests/fixtures/homekit_controller/arlo_baby.json @@ -0,0 +1,484 @@ +[ + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "value": "ArloBabyA0", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "value": "Netgear, Inc", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "value": "00A0000000000", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "value": "ABC1000", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "value": "1.10.931", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 20, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 21, + "value": "1.1.0", + "perms": [ + "pr" + ], + "format": "string" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 100, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 106, + "value": "AQEB", + "perms": [ + "pr", + "ev" + ], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 101, + "value": "AY8BAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQKABwICOAQDAR4DCwECAAUCAsADAwEeAwsBAgAEAgIAAwMBHgMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 102, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 103, + "value": "AgEAAgEBAgEC", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 104, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 108, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 110, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 116, + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 111, + "value": "AWgBAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 112, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 113, + "value": "AgEAAgEBAgEC", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 114, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 118, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + } + ] + }, + { + "type": "00000112-0000-1000-8000-0026BB765291", + "iid": 300, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 302, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + } + ] + }, + { + "type": "00000113-0000-1000-8000-0026BB765291", + "iid": 400, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 402, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + }, + { + "type": "00000119-0000-1000-8000-0026BB765291", + "iid": 403, + "value": 50, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ] + }, + { + "type": "00000085-0000-1000-8000-0026BB765291", + "iid": 500, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 501, + "value": "Motion", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 502, + "value": false, + "perms": [ + "pr", + "ev" + ], + "format": "bool" + } + ] + }, + { + "type": "00000096-0000-1000-8000-0026BB765291", + "iid": 700, + "characteristics": [ + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 701, + "value": 82, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 702, + "value": 0, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 703, + "value": 0, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + } + ] + }, + { + "type": "0000008D-0000-1000-8000-0026BB765291", + "iid": 800, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 801, + "value": "Air Quality", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000095-0000-1000-8000-0026BB765291", + "iid": 802, + "value": 1, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 5, + "minStep": 1 + } + ] + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 900, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 901, + "value": "Humidity", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 902, + "value": 60.099998, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 1000, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1001, + "value": "Temperature", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 1002, + "value": 24.0, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 0.1, + "unit": "celsius" + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 1100, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1101, + "value": "Nightlight", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 1102, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 1103, + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 1104, + "value": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0, + "unit": "arcdegrees" + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 1105, + "value": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/fixtures/myq/devices.json b/tests/fixtures/myq/devices.json index f7c65c6bb20..1e731ffe204 100644 --- a/tests/fixtures/myq/devices.json +++ b/tests/fixtures/myq/devices.json @@ -1,5 +1,5 @@ { - "count" : 4, + "count" : 6, "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices", "items" : [ { @@ -128,6 +128,36 @@ "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", "device_type" : "wifigaragedooropener", "created_date" : "2020-02-10T23:11:47.487" - } + }, + { + "serial_number" : "garage_light_off", + "state" : { + "last_status" : "2020-03-30T02:48:45.7501595Z", + "online" : true, + "lamp_state" : "off", + "last_update" : "2020-03-26T15:45:31.4713796Z" + }, + "parent_device_id" : "gateway_serial", + "device_platform" : "myq", + "name" : "Garage Door Light Off", + "device_family" : "lamp", + "device_type" : "lamp", + "created_date" : "2020-02-10T23:11:47.487" + }, + { + "serial_number" : "garage_light_on", + "state" : { + "last_status" : "2020-03-30T02:48:45.7501595Z", + "online" : true, + "lamp_state" : "on", + "last_update" : "2020-03-26T15:45:31.4713796Z" + }, + "parent_device_id" : "gateway_serial", + "device_platform" : "myq", + "name" : "Garage Door Light On", + "device_family" : "lamp", + "device_type" : "lamp", + "created_date" : "2020-02-10T23:11:47.487" + } ] } diff --git a/tests/fixtures/mysensors/distance_sensor_state.json b/tests/fixtures/mysensors/distance_sensor_state.json new file mode 100644 index 00000000000..ff8b246b880 --- /dev/null +++ b/tests/fixtures/mysensors/distance_sensor_state.json @@ -0,0 +1,22 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 15, + "description": "", + "values": { + "13": "15", + "43": "cm" + } + } + }, + "type": 17, + "sketch_name": "Distance Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/energy_sensor_state.json b/tests/fixtures/mysensors/energy_sensor_state.json new file mode 100644 index 00000000000..063083c9c1e --- /dev/null +++ b/tests/fixtures/mysensors/energy_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "18": "18000" + } + } + }, + "type": 17, + "sketch_name": "Energy Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/sound_sensor_state.json b/tests/fixtures/mysensors/sound_sensor_state.json new file mode 100644 index 00000000000..35651243250 --- /dev/null +++ b/tests/fixtures/mysensors/sound_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 33, + "description": "", + "values": { + "37": "10" + } + } + }, + "type": 17, + "sketch_name": "Sound Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/temperature_sensor_state.json b/tests/fixtures/mysensors/temperature_sensor_state.json new file mode 100644 index 00000000000..4367be6a3cd --- /dev/null +++ b/tests/fixtures/mysensors/temperature_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 6, + "description": "", + "values": { + "0": "20.0" + } + } + }, + "type": 17, + "sketch_name": "Temperature Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/renault/no_data.json b/tests/fixtures/renault/no_data.json new file mode 100644 index 00000000000..7b78844ca99 --- /dev/null +++ b/tests/fixtures/renault/no_data.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": {} + } +} 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." } }, { diff --git a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json index 65725606e1c..bde7c90e1e4 100644 --- a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json +++ b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json @@ -1,48 +1,104 @@ { - "nodeId": 5, + "nodeId": 20, "index": 0, "installerIcon": 6656, "userIcon": 6656, "status": 4, "ready": true, - "deviceClass": { - "basic": { "key": 4, "label": "Routing Slave" }, - "generic": { "key": 17, "label": "Routing Slave" }, - "specific": { "key": 7, "label": "Routing Slave" }, - "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] - }, "isListening": true, - "isFrequentListening": false, "isRouting": true, - "maxBaudRate": 40000, "isSecure": false, - "version": 4, - "isBeaming": true, "manufacturerId": 345, - "productId": 83, + "productId": 82, "productType": 3, - "firmwareVersion": "7.2", + "firmwareVersion": "71.0", "zwavePlusVersion": 1, - "nodeType": 0, - "roleType": 5, "deviceConfig": { - "manufacturerId": 345, + "filename": "/data/db/devices/0x0159/zmnhcd_4.1.json", + "isEmbedded": true, "manufacturer": "Qubino", - "label": "ZMNHOD", - "description": "Flush Shutter DC", - "devices": [{ "productType": "0x0003", "productId": "0x0053" }], - "firmwareVersion": { "min": "0.0", "max": "255.255" }, - "paramInformation": { "_map": {} } + "manufacturerId": 345, + "label": "ZMNHCD", + "description": "Flush Shutter", + "devices": [ + { + "productType": 3, + "productId": 82 + } + ], + "firmwareVersion": { + "min": "4.1", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } }, - "label": "ZMNHOD", - "neighbors": [1, 2], - "interviewAttempts": 1, + "label": "ZMNHCD", + "interviewAttempts": 0, "endpoints": [ - { "nodeId": 5, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + { + "nodeId": 20, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + } + } ], - "commandClasses": [], "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ] + } + }, { "endpoint": 0, "commandClass": 38, @@ -54,10 +110,14 @@ "type": "number", "readable": true, "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, - "max": 99, - "label": "Target value" - } + "max": 99 + }, + "value": 99 }, { "endpoint": 0, @@ -84,11 +144,11 @@ "type": "number", "readable": true, "writeable": false, + "label": "Current value", "min": 0, - "max": 99, - "label": "Current value" + "max": 99 }, - "value": "unknown" + "value": 0 }, { "endpoint": 0, @@ -102,7 +162,9 @@ "readable": true, "writeable": true, "label": "Perform a level change (Up)", - "ccSpecific": { "switchType": 2 } + "ccSpecific": { + "switchType": 2 + } } }, { @@ -117,146 +179,9 @@ "readable": true, "writeable": true, "label": "Perform a level change (Down)", - "ccSpecific": { "switchType": 2 } - } - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value" - }, - "value": "unknown" - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value" - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "manufacturerId", - "propertyName": "manufacturerId", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Manufacturer ID" - }, - "value": 345 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productType", - "propertyName": "productType", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product type" - }, - "value": 3 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productId", - "propertyName": "productId", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product ID" - }, - "value": 83 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "libraryType", - "propertyName": "libraryType", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Library type" - }, - "value": 3 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "protocolVersion", - "propertyName": "protocolVersion", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" - }, - "value": "4.38" - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" - }, - "value": ["7.2"] - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "hardwareVersion", - "propertyName": "hardwareVersion", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip hardware version" + "ccSpecific": { + "switchType": 2 + } } }, { @@ -273,29 +198,14 @@ "readable": true, "writeable": false, "label": "Electric Consumed [kWh]", - "unit": "kWh", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "deltaTime", - "propertyKey": 65537, - "propertyName": "deltaTime", - "propertyKeyName": "Electric_kWh_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [kWh] (prev. time delta)", - "unit": "s", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } - }, - "value": 0 + "value": 7.9 }, { "endpoint": 0, @@ -311,27 +221,12 @@ "readable": true, "writeable": false, "label": "Electric Consumed [W]", - "unit": "W", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "deltaTime", - "propertyKey": 66049, - "propertyName": "deltaTime", - "propertyKeyName": "Electric_W_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [W] (prev. time delta)", - "unit": "s", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" }, "value": 0 }, @@ -349,119 +244,31 @@ "label": "Reset accumulated values" } }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "previousValue", - "propertyKey": 65537, - "propertyName": "previousValue", - "propertyKeyName": "Electric_kWh_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [kWh] (prev. value)", - "unit": "kWh", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } - } - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "previousValue", - "propertyKey": 66049, - "propertyName": "previousValue", - "propertyKeyName": "Electric_W_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [W] (prev. value)", - "unit": "W", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmType", - "propertyName": "alarmType", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Alarm Type" - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmLevel", - "propertyName": "alarmLevel", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Alarm Level" - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Power Management", - "propertyKey": "Over-load status", - "propertyName": "Power Management", - "propertyKeyName": "Over-load status", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Over-load status", - "states": { "0": "idle", "8": "Over-load detected" }, - "ccSpecific": { "notificationType": 8 } - }, - "value": 0 - }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 10, - "propertyName": "Activate/deactivate functions ALL ON / ALL OFF", + "propertyName": "ALL ON/ALL OFF", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 2, - "min": 0, - "max": 65535, + "description": "Responds to commands ALL ON / ALL OFF from Main Controller", + "label": "ALL ON/ALL OFF", "default": 255, - "format": 1, - "allowManualEntry": false, + "min": 0, + "max": 255, "states": { - "0": "ALL ON is not active, ALL OFF is not active", + "0": "ALL ON is not active ALL OFF is not active", "1": "ALL ON is not active ALL OFF active", "2": "ALL ON is not active ALL OFF is not active", "255": "ALL ON active, ALL OFF active" }, - "label": "Activate/deactivate functions ALL ON / ALL OFF", + "valueSize": 2, + "format": 0, + "allowManualEntry": false, "isFromConfig": true }, "value": 255 @@ -471,19 +278,20 @@ "commandClass": 112, "commandClassName": "Configuration", "property": 40, - "propertyName": "Power report (Watts) on power change for Q1 or Q2", + "propertyName": "Power reporting in watts on power change", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Power consumption change threshold for sending updates", + "label": "Power reporting in watts on power change", + "default": 1, "min": 0, "max": 100, - "default": 1, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Power report (Watts) on power change for Q1 or Q2", "isFromConfig": true }, "value": 10 @@ -493,19 +301,20 @@ "commandClass": 112, "commandClassName": "Configuration", "property": 42, - "propertyName": "Power report (Watts) by time interval for Q1 or Q2", + "propertyName": "Power reporting in Watts by time interval", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "label": "Power reporting in Watts by time interval", + "default": 300, "min": 0, "max": 32767, - "default": 300, + "unit": "seconds", + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Power report (Watts) by time interval for Q1 or Q2", "isFromConfig": true }, "value": 0 @@ -521,17 +330,18 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Operation Mode (Shutter or Venetian)", + "label": "Operating modes", + "default": 0, "min": 0, "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, "states": { - "0": "Shutter mode.", + "0": "Shutter mode", "1": "Venetian mode (up/down and slate rotation)" }, - "label": "Operating modes", + "valueSize": 1, + "format": 1, + "allowManualEntry": false, "isFromConfig": true }, "value": 0 @@ -547,16 +357,18 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "description": "Slat full turn time in tenths of a second.", + "label": "Slats tilting full turn time", + "default": 150, "min": 0, "max": 32767, - "default": 150, + "unit": "tenths of a second", + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Slats tilting full turn time", "isFromConfig": true }, - "value": 630 + "value": 150 }, { "endpoint": 0, @@ -569,43 +381,22 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 1, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Return to previous position only with Z-wave", - "1": "Return to previous position with Z-wave or button" - }, + "description": "Slats position after up/down movement.", "label": "Slats position", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Previous position for Z-wave control only", + "1": "Return to previous position in all cases" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, "isFromConfig": true }, "value": 1 }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 74, - "propertyName": "Motor moving up/down time", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 2, - "min": 0, - "max": 32767, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "Motor moving up/down time", - "isFromConfig": true - }, - "value": 0 - }, { "endpoint": 0, "commandClass": 112, @@ -617,36 +408,41 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Power threshold to be interpreted when motor reach the limit switch", + "label": "Motor operation detection", + "default": 10, "min": 0, - "max": 100, - "default": 6, + "max": 127, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Motor operation detection", "isFromConfig": true }, - "value": 10 + "value": 30 }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 78, - "propertyName": "Forced Shutter DC calibration", + "propertyName": "Forced Shutter calibration", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, + "description": "Enters calibration mode if set to 1", + "label": "Forced Shutter calibration", "default": 0, - "format": 1, + "min": 0, + "max": 1, + "states": { + "0": "Default", + "1": "Start Calibration Process" + }, + "valueSize": 1, + "format": 0, "allowManualEntry": false, - "states": { "0": "Default", "1": "Start calibration process." }, - "label": "Forced Shutter DC calibration", "isFromConfig": true }, "value": 0 @@ -662,57 +458,38 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 3, - "max": 50, - "default": 8, - "format": 0, - "allowManualEntry": true, + "description": "Time delay for detecting motor errors", "label": "Power consumption max delay time", - "isFromConfig": true - }, - "value": 8 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 86, - "propertyName": "Power consumption at limit switch delay time", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 3, - "max": 50, "default": 8, + "min": 0, + "max": 50, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Power consumption at limit switch delay time", "isFromConfig": true }, - "value": 8 + "value": 30 }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 90, - "propertyName": "Time delay for next motor movement", + "propertyName": "Relay delay time", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Defines the minimum time delay between next motor movement", + "label": "Relay delay time", + "default": 5, "min": 1, "max": 30, - "default": 5, + "unit": "milliseconds", + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Time delay for next motor movement", "isFromConfig": true }, "value": 5 @@ -728,13 +505,14 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "description": "Adds or removes an offset from the measured temperature.", + "label": "Temperature sensor offset settings", + "default": 32536, "min": 1, "max": 32536, - "default": 32536, + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Temperature sensor offset settings", "isFromConfig": true }, "value": 32536 @@ -750,16 +528,373 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Threshold for sending temperature change reports", + "label": "Digital temperature sensor reporting", + "default": 5, "min": 0, "max": 127, - "default": 5, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Digital temperature sensor reporting", "isFromConfig": true }, "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 74, + "propertyName": "Motor moving up/down time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Shutter motor moving time of complete opening or complete closing", + "label": "Motor moving up/down time", + "default": 0, + "min": 0, + "max": 32767, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 80, + "propertyName": "Reporting to Controller", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Defines if reporting regarding power level, etc is reported to controller.", + "label": "Reporting to Controller", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Reporting to Controller Disabled", + "1": "Reporting to Controller Enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 86, + "propertyName": "Power consumption at limit switch delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the time delay for detecting limit switches", + "label": "Power consumption at limit switch delay time", + "default": 8, + "min": 3, + "max": 50, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "unknown", + "propertyName": "Power Management", + "propertyKeyName": "unknown", + "ccVersion": 5, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 254 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-load status", + "propertyName": "Power Management", + "propertyKeyName": "Over-load status", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-load status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Over-load detected" + } + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 345 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 82 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.38" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "71.0", + "71.0" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 2 } - ] + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 4, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0159:0x0003:0x0052:71.0", + "statistics": { + "commandsTX": 17, + "commandsRX": 57, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } } diff --git a/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json b/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json new file mode 100644 index 00000000000..2b4a3a88984 --- /dev/null +++ b/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json @@ -0,0 +1,568 @@ +{ + "nodeId": 62, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 340, + "productId": 1, + "productType": 5, + "firmwareVersion": "1.3", + "zwavePlusVersion": 1, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 62, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 10, + "label": "Lockbox" + }, + "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 48, + "commandClassName": "Binary Sensor", + "property": "Door/Window", + "propertyName": "Door/Window", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Door/Window", + "ccSpecific": { + "sensorType": 10 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "closed" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "unlocked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "closed" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Door state", + "propertyName": "Access Control", + "propertyKeyName": "Door state", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Door state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "22": "Window/door is open", + "23": "Window/door is closed" + } + }, + "value": 23 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 340 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.3"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 10, + "label": "Lockbox" + }, + "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 48, + "name": "Binary Sensor", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": true + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0154:0x0005:0x0001:1.3", + "statistics": { + "commandsTX": 1, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } +} diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index c5e9f5880c4..79b558e5083 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -120,6 +120,25 @@ def test_url(): assert schema(value) +def test_url_no_path(): + """Test URL.""" + schema = vol.Schema(cv.url_no_path) + + for value in ( + "https://localhost/test/index.html", + "http://home-assistant.io/test/", + ): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ( + "http://localhost", + "http://home-assistant.io", + "https://community.home-assistant.io/", + ): + assert schema(value) + + def test_platform_config(): """Test platform config validation.""" options = ({}, {"hello": "world"}) diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 1d3be2ca98d..d138a5381da 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -98,6 +98,62 @@ async def test_periodic_write(hass): assert not mock_write_data.called +async def test_save_persistent_states(hass): + """Test that we cancel the currently running job, save the data, and verify the perdiodic job continues.""" + data = await RestoreStateData.async_get_instance(hass) + await hass.async_block_till_done() + await data.store.async_save([]) + + # Emulate a fresh load + hass.data[DATA_RESTORE_STATE_TASK] = None + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await entity.async_get_last_state() + await hass.async_block_till_done() + + # Startup Save + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + # Not quite the first interval + assert not mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await RestoreStateData.async_save_persistent_states(hass) + await hass.async_block_till_done() + + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + # Verify still saving + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + # Verify normal shutdown + assert mock_write_data.called + + async def test_hass_starting(hass): """Test that we cache data.""" hass.state = CoreState.starting diff --git a/tests/test_config.py b/tests/test_config.py index 96196c943aa..441029d27dc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -215,6 +215,19 @@ def test_core_config_schema(): ) +def test_core_config_schema_internal_external_warning(caplog): + """Test that we warn for internal/external URL with path.""" + config_util.CORE_CONFIG_SCHEMA( + { + "external_url": "https://www.example.com/bla", + "internal_url": "http://example.local/yo", + } + ) + + assert "Invalid external_url set" in caplog.text + assert "Invalid internal_url set" in caplog.text + + def test_customize_dict_schema(): """Test basic customize config validation.""" values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"}) diff --git a/tests/test_core.py b/tests/test_core.py index 77ec07e6a63..641a5e0dfda 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1374,6 +1374,33 @@ async def test_additional_data_in_core_config(hass, hass_storage): assert config.location_name == "Test Name" +async def test_incorrect_internal_external_url(hass, hass_storage, caplog): + """Test that we warn when detecting invalid internal/extenral url.""" + config = ha.Config(hass) + + hass_storage[ha.CORE_STORAGE_KEY] = { + "version": 1, + "data": { + "internal_url": None, + "external_url": None, + }, + } + await config.async_load() + assert "Invalid external_url set" not in caplog.text + assert "Invalid internal_url set" not in caplog.text + + hass_storage[ha.CORE_STORAGE_KEY] = { + "version": 1, + "data": { + "internal_url": "https://community.home-assistant.io/profile", + "external_url": "https://www.home-assistant.io/blue", + }, + } + await config.async_load() + assert "Invalid external_url set" in caplog.text + assert "Invalid internal_url set" in caplog.text + + async def test_start_events(hass): """Test events fired when starting Home Assistant.""" hass.state = ha.CoreState.not_running diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 384db20d2d4..010b82dc3a2 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -5,10 +5,12 @@ Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, + VOLUME_CUBIC_METERS, ) from tests.common import MockEntity @@ -22,7 +24,15 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen dioxide + sensor.DEVICE_CLASS_NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen monoxide + sensor.DEVICE_CLASS_NITROUS_OXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen oxide + sensor.DEVICE_CLASS_OZONE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of ozone + sensor.DEVICE_CLASS_PM1: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM1 + sensor.DEVICE_CLASS_PM10: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM10 + sensor.DEVICE_CLASS_PM25: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM2.5 sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm) + sensor.DEVICE_CLASS_SULPHUR_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of sulphur dioxide sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) @@ -30,6 +40,7 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) + sensor.DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, # gas (m³) } ENTITIES = {} @@ -61,7 +72,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockSensor(MockEntity): +class MockSensor(MockEntity, sensor.SensorEntity): """Mock Sensor class.""" @property @@ -70,6 +81,21 @@ class MockSensor(MockEntity): return self._handle("device_class") @property - def unit_of_measurement(self): - """Return the unit_of_measurement of this sensor.""" - return self._handle("unit_of_measurement") + def last_reset(self): + """Return the last_reset of this sensor.""" + return self._handle("last_reset") + + @property + def native_unit_of_measurement(self): + """Return the native unit_of_measurement of this sensor.""" + return self._handle("native_unit_of_measurement") + + @property + def native_value(self): + """Return the native value of this sensor.""" + return self._handle("native_value") + + @property + def state_class(self): + """Return the state class of this sensor.""" + return self._handle("state_class") diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 2c596d92e5b..3cbf5b72130 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -3,6 +3,8 @@ import pytest from homeassistant.const import ( + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_GALLONS, VOLUME_LITERS, @@ -47,3 +49,21 @@ def test_convert_from_gallons(): """Test conversion from gallons to other units.""" gallons = 5 assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == 18.925 + + +def test_convert_from_cubic_meters(): + """Test conversion from cubic meter to other units.""" + cubic_meters = 5 + assert ( + volume_util.convert(cubic_meters, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET) + == 176.5733335 + ) + + +def test_convert_from_cubic_feet(): + """Test conversion from cubic feet to cubic meters to other units.""" + cubic_feets = 500 + assert ( + volume_util.convert(cubic_feets, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS) + == 14.1584233 + )